Qubx 0.6.49__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.52__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/backtester/data.py CHANGED
@@ -139,26 +139,31 @@ class SimulatedDataProvider(IDataProvider):
139
139
  bars = []
140
140
 
141
141
  # - if no records, return empty list to avoid exception from infer_series_frequency
142
- if not records:
142
+ if not records or records is None:
143
143
  return bars
144
144
 
145
- _data_tf = infer_series_frequency([r.time for r in records[:50]])
146
- timeframe_ns = _data_tf.item()
147
-
148
- if records is not None:
149
- for r in records:
150
- # _b_ts_0 = np.datetime64(r.time, "ns").item()
151
- _b_ts_0 = r.time
152
- _b_ts_1 = _b_ts_0 + timeframe_ns - self._open_close_time_indent_ns
153
-
154
- if _b_ts_0 <= cut_time_ns and cut_time_ns < _b_ts_1:
155
- break
156
-
157
- bars.append(
158
- Bar(
159
- _b_ts_0, r.data["open"], r.data["high"], r.data["low"], r.data["close"], r.data.get("volume", 0)
160
- )
145
+ if len(records) > 1:
146
+ _data_tf = infer_series_frequency([r.time for r in records[:50]])
147
+ timeframe_ns = _data_tf.item()
148
+
149
+ for r in records:
150
+ _b_ts_0 = r.time
151
+ _b_ts_1 = _b_ts_0 + timeframe_ns - self._open_close_time_indent_ns
152
+
153
+ if _b_ts_0 <= cut_time_ns and cut_time_ns < _b_ts_1:
154
+ break
155
+
156
+ bars.append(
157
+ Bar(
158
+ _b_ts_0,
159
+ r.data["open"],
160
+ r.data["high"],
161
+ r.data["low"],
162
+ r.data["close"],
163
+ r.data.get("volume", 0),
164
+ r.data.get("bought_volume", 0),
161
165
  )
166
+ )
162
167
 
163
168
  return bars
164
169
 
@@ -48,6 +48,7 @@ def simulate(
48
48
  portfolio_log_freq: str = "5Min",
49
49
  parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
50
50
  emission: EmissionConfig | None = None,
51
+ run_separate_instruments: bool = False,
51
52
  ) -> list[TradingSessionResult]:
52
53
  """
53
54
  Backtest utility for trading strategies or signals using historical data.
@@ -73,6 +74,7 @@ def simulate(
73
74
  - portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
74
75
  - parallel_backend (Literal["loky", "multiprocessing"]): Backend for parallel processing, default is "multiprocessing".
75
76
  - emission (EmissionConfig | None): Configuration for metric emitters, default is None.
77
+ - run_separate_instruments (bool): If True, creates separate simulation setups for each instrument, default is False.
76
78
 
77
79
  Returns:
78
80
  - list[TradingSessionResult]: A list of TradingSessionResult objects containing the results of each simulation setup.
@@ -109,6 +111,7 @@ def simulate(
109
111
  commissions=commissions,
110
112
  signal_timeframe=signal_timeframe,
111
113
  accurate_stop_orders_execution=accurate_stop_orders_execution,
114
+ run_separate_instruments=run_separate_instruments,
112
115
  )
113
116
  if not simulation_setups:
114
117
  logger.error(
@@ -117,6 +120,10 @@ def simulate(
117
120
  )
118
121
  raise SimulationError(_msg)
119
122
 
123
+ # - inform about separate instruments mode
124
+ if run_separate_instruments and len(simulation_setups) > 1:
125
+ logger.info(f"Running separate simulations for each instrument. Total simulations: {len(simulation_setups)}")
126
+
120
127
  # - preprocess start and stop and convert to datetime if necessary
121
128
  if stop is None:
122
129
  # - check stop time : here we try to backtest till now (may be we need to get max available time from data reader ?)
@@ -160,24 +167,50 @@ def _run_setups(
160
167
  _main_loop_silent = len(strategies_setups) == 1
161
168
  n_jobs = 1 if _main_loop_silent else n_jobs
162
169
 
163
- reports = ProgressParallel(
164
- n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend=parallel_backend
165
- )(
166
- delayed(_run_setup)(
167
- id,
168
- f"Simulated-{id}",
169
- setup,
170
- data_setup,
171
- start,
172
- stop,
173
- silent,
174
- show_latency_report,
175
- portfolio_log_freq,
176
- emission,
170
+ if n_jobs == 1:
171
+ reports = [
172
+ _run_setup(
173
+ id,
174
+ f"Simulated-{id}",
175
+ setup,
176
+ data_setup,
177
+ start,
178
+ stop,
179
+ silent,
180
+ show_latency_report,
181
+ portfolio_log_freq,
182
+ emission,
183
+ )
184
+ for id, setup in enumerate(strategies_setups)
185
+ ]
186
+ else:
187
+ reports = ProgressParallel(
188
+ n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend=parallel_backend
189
+ )(
190
+ delayed(_run_setup)(
191
+ id,
192
+ f"Simulated-{id}",
193
+ setup,
194
+ data_setup,
195
+ start,
196
+ stop,
197
+ silent,
198
+ show_latency_report,
199
+ portfolio_log_freq,
200
+ emission,
201
+ )
202
+ for id, setup in enumerate(strategies_setups)
177
203
  )
178
- for id, setup in enumerate(strategies_setups)
179
- )
180
- return reports # type: ignore
204
+
205
+ # Filter out None results and log warnings for failed simulations
206
+ successful_reports = []
207
+ for i, report in enumerate(reports):
208
+ if report is None:
209
+ logger.warning(f"Simulation setup {i} failed - skipping from results")
210
+ else:
211
+ successful_reports.append(report)
212
+
213
+ return successful_reports
181
214
 
182
215
 
183
216
  def _run_setup(
@@ -191,48 +224,58 @@ def _run_setup(
191
224
  show_latency_report: bool,
192
225
  portfolio_log_freq: str,
193
226
  emission: EmissionConfig | None = None,
194
- ) -> TradingSessionResult:
195
- # Create metric emitter if configured
196
- emitter = None
197
- if emission is not None:
198
- emitter = create_metric_emitters(emission, setup.name)
199
-
200
- runner = SimulationRunner(
201
- setup=setup,
202
- data_config=data_setup,
203
- start=start,
204
- stop=stop,
205
- account_id=account_id,
206
- portfolio_log_freq=portfolio_log_freq,
207
- emitter=emitter,
208
- )
227
+ ) -> TradingSessionResult | None:
228
+ try:
229
+ # Create metric emitter if configured
230
+ emitter = None
231
+ if emission is not None:
232
+ emitter = create_metric_emitters(emission, setup.name)
209
233
 
210
- # - we want to see simulate time in log messages
211
- QubxLogConfig.setup_logger(
212
- level=QubxLogConfig.get_log_level(), custom_formatter=SimulatedLogFormatter(runner.ctx).formatter
213
- )
234
+ runner = SimulationRunner(
235
+ setup=setup,
236
+ data_config=data_setup,
237
+ start=start,
238
+ stop=stop,
239
+ account_id=account_id,
240
+ portfolio_log_freq=portfolio_log_freq,
241
+ emitter=emitter,
242
+ )
214
243
 
215
- runner.run(silent=silent)
216
-
217
- # - service latency report
218
- if show_latency_report:
219
- runner.print_latency_report()
220
-
221
- return TradingSessionResult(
222
- setup_id,
223
- setup.name,
224
- start,
225
- stop,
226
- setup.exchanges,
227
- setup.instruments,
228
- setup.capital,
229
- setup.base_currency,
230
- setup.commissions,
231
- runner.logs_writer.get_portfolio(as_plain_dataframe=True),
232
- runner.logs_writer.get_executions(),
233
- runner.logs_writer.get_signals(),
234
- strategy_class=runner.strategy_class,
235
- parameters=runner.strategy_params,
236
- is_simulation=True,
237
- author=get_current_user(),
238
- )
244
+ # - we want to see simulate time in log messages
245
+ QubxLogConfig.setup_logger(
246
+ level=QubxLogConfig.get_log_level(), custom_formatter=SimulatedLogFormatter(runner.ctx).formatter
247
+ )
248
+
249
+ runner.run(silent=silent)
250
+
251
+ # - service latency report
252
+ if show_latency_report:
253
+ runner.print_latency_report()
254
+
255
+ # Convert commissions to the expected type for TradingSessionResult
256
+ commissions_for_result = setup.commissions
257
+ if isinstance(commissions_for_result, dict):
258
+ # Filter out None values to match TradingSessionResult expected type
259
+ commissions_for_result = {k: v for k, v in commissions_for_result.items() if v is not None}
260
+
261
+ return TradingSessionResult(
262
+ setup_id,
263
+ setup.name,
264
+ start,
265
+ stop,
266
+ setup.exchanges,
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(),
274
+ strategy_class=runner.strategy_class,
275
+ parameters=runner.strategy_params,
276
+ is_simulation=True,
277
+ author=get_current_user(),
278
+ )
279
+ except Exception as e:
280
+ logger.error(f"Simulation setup {setup_id} failed with error: {e}")
281
+ return None
qubx/backtester/utils.py CHANGED
@@ -419,6 +419,7 @@ def recognize_simulation_configuration(
419
419
  commissions: str | dict[str, str | None] | None,
420
420
  signal_timeframe: str,
421
421
  accurate_stop_orders_execution: bool,
422
+ run_separate_instruments: bool = False,
422
423
  ) -> list[SimulationSetup]:
423
424
  """
424
425
  Recognize and create setups based on the provided simulation configuration.
@@ -438,6 +439,7 @@ def recognize_simulation_configuration(
438
439
  - commissions (str): The commission structure to be applied.
439
440
  - signal_timeframe (str): Timeframe for generated signals.
440
441
  - accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
442
+ - run_separate_instruments (bool): If True, creates separate setups for each instrument.
441
443
 
442
444
  Returns:
443
445
  - list[SimulationSetup]: A list of SimulationSetup objects, each representing a
@@ -458,7 +460,7 @@ def recognize_simulation_configuration(
458
460
  r.extend(
459
461
  recognize_simulation_configuration(
460
462
  _n + n, v, instruments, exchanges, capital, basic_currency, commissions,
461
- signal_timeframe, accurate_stop_orders_execution
463
+ signal_timeframe, accurate_stop_orders_execution, run_separate_instruments
462
464
  )
463
465
  )
464
466
 
@@ -474,45 +476,85 @@ def recognize_simulation_configuration(
474
476
  _t = SetupTypes.STRATEGY_AND_TRACKER
475
477
 
476
478
  # - extract actual symbols that have signals
477
- r.append(
478
- SimulationSetup(
479
- _t, name, _s, c1, # type: ignore
480
- _sniffer._pick_instruments(instruments, _s) if _sniffer._is_signal(c0) else instruments,
481
- exchanges, capital, basic_currency, commissions,
482
- signal_timeframe, accurate_stop_orders_execution
479
+ setup_instruments = _sniffer._pick_instruments(instruments, _s) if _sniffer._is_signal(c0) else instruments
480
+
481
+ if run_separate_instruments:
482
+ # Create separate setups for each instrument
483
+ for instrument in setup_instruments:
484
+ r.append(
485
+ SimulationSetup(
486
+ _t, f"{name}/{instrument.symbol}", _s, c1, # type: ignore
487
+ [instrument],
488
+ exchanges, capital, basic_currency, commissions,
489
+ signal_timeframe, accurate_stop_orders_execution
490
+ )
491
+ )
492
+ else:
493
+ r.append(
494
+ SimulationSetup(
495
+ _t, name, _s, c1, # type: ignore
496
+ setup_instruments,
497
+ exchanges, capital, basic_currency, commissions,
498
+ signal_timeframe, accurate_stop_orders_execution
499
+ )
483
500
  )
484
- )
485
501
  else:
486
502
  for j, s in enumerate(configs):
487
503
  r.extend(
488
504
  recognize_simulation_configuration(
489
505
  # name + "/" + str(j), s, instruments, exchange, capital, basic_currency, commissions
490
506
  name, s, instruments, exchanges, capital, basic_currency, commissions, # type: ignore
491
- signal_timeframe, accurate_stop_orders_execution
507
+ signal_timeframe, accurate_stop_orders_execution, run_separate_instruments
492
508
  )
493
509
  )
494
510
 
495
511
  elif _sniffer._is_strategy(configs):
496
- r.append(
497
- SimulationSetup(
498
- SetupTypes.STRATEGY,
499
- name, configs, None, instruments,
500
- exchanges, capital, basic_currency, commissions,
501
- signal_timeframe, accurate_stop_orders_execution
512
+ if run_separate_instruments:
513
+ # Create separate setups for each instrument
514
+ for instrument in instruments:
515
+ r.append(
516
+ SimulationSetup(
517
+ SetupTypes.STRATEGY,
518
+ f"{name}/{instrument.symbol}", configs, None, [instrument],
519
+ exchanges, capital, basic_currency, commissions,
520
+ signal_timeframe, accurate_stop_orders_execution
521
+ )
522
+ )
523
+ else:
524
+ r.append(
525
+ SimulationSetup(
526
+ SetupTypes.STRATEGY,
527
+ name, configs, None, instruments,
528
+ exchanges, capital, basic_currency, commissions,
529
+ signal_timeframe, accurate_stop_orders_execution
530
+ )
502
531
  )
503
- )
504
532
 
505
533
  elif _sniffer._is_signal(configs):
506
534
  # - check structure of signals
507
535
  c1 = _sniffer._check_signals_structure(instruments, configs) # type: ignore
508
- r.append(
509
- SimulationSetup(
510
- SetupTypes.SIGNAL,
511
- name, c1, None, _sniffer._pick_instruments(instruments, c1),
512
- exchanges, capital, basic_currency, commissions,
513
- signal_timeframe, accurate_stop_orders_execution
536
+ setup_instruments = _sniffer._pick_instruments(instruments, c1)
537
+
538
+ if run_separate_instruments:
539
+ # Create separate setups for each instrument
540
+ for instrument in setup_instruments:
541
+ r.append(
542
+ SimulationSetup(
543
+ SetupTypes.SIGNAL,
544
+ f"{name}/{instrument.symbol}", c1, None, [instrument],
545
+ exchanges, capital, basic_currency, commissions,
546
+ signal_timeframe, accurate_stop_orders_execution
547
+ )
548
+ )
549
+ else:
550
+ r.append(
551
+ SimulationSetup(
552
+ SetupTypes.SIGNAL,
553
+ name, c1, None, setup_instruments,
554
+ exchanges, capital, basic_currency, commissions,
555
+ signal_timeframe, accurate_stop_orders_execution
556
+ )
514
557
  )
515
- )
516
558
 
517
559
  # fmt: on
518
560
  return r
qubx/core/loggers.py CHANGED
@@ -10,10 +10,8 @@ from qubx.core.basics import (
10
10
  Position,
11
11
  TargetPosition,
12
12
  )
13
-
14
13
  from qubx.core.series import time_as_nsec
15
14
  from qubx.core.utils import recognize_timeframe
16
-
17
15
  from qubx.utils.misc import Stopwatch
18
16
  from qubx.utils.time import convert_tf_str_td64, floor_t64
19
17
 
@@ -21,14 +19,14 @@ _SW = Stopwatch()
21
19
 
22
20
 
23
21
  class LogsWriter:
24
- account_id: str
25
- strategy_id: str
26
- run_id: str
27
-
28
22
  """
29
23
  Log writer interface with default implementation
30
24
  """
31
25
 
26
+ account_id: str
27
+ strategy_id: str
28
+ run_id: str
29
+
32
30
  def __init__(self, account_id: str, strategy_id: str, run_id: str) -> None:
33
31
  self.account_id = account_id
34
32
  self.strategy_id = strategy_id
@@ -39,7 +39,8 @@ from qubx.core.series import Bar, OrderBook, Quote, Trade
39
39
 
40
40
 
41
41
  class ProcessingManager(IProcessingManager):
42
- MAX_NUMBER_OF_STRATEGY_FAILURES = 10
42
+ MAX_NUMBER_OF_STRATEGY_FAILURES: int = 10
43
+ DATA_READY_TIMEOUT_SECONDS: int = 60
43
44
 
44
45
  _context: IStrategyContext
45
46
  _strategy: IStrategy
@@ -67,6 +68,7 @@ class ProcessingManager(IProcessingManager):
67
68
  _trig_bar_freq_nsec: int | None = None
68
69
  _cur_sim_step: int | None = None
69
70
  _updated_instruments: set[Instrument] = set()
71
+ _data_ready_start_time: dt_64 | None = None
70
72
 
71
73
  def __init__(
72
74
  self,
@@ -111,6 +113,7 @@ class ProcessingManager(IProcessingManager):
111
113
  self._strategy_name = strategy.__class__.__name__
112
114
  self._trig_bar_freq_nsec = None
113
115
  self._updated_instruments = set()
116
+ self._data_ready_start_time = None
114
117
 
115
118
  def set_fit_schedule(self, schedule: str) -> None:
116
119
  rule = process_schedule_spec(schedule)
@@ -344,9 +347,56 @@ class ProcessingManager(IProcessingManager):
344
347
 
345
348
  def _is_data_ready(self) -> bool:
346
349
  """
347
- Check if at least one update was received for all instruments in the context.
350
+ Check if strategy can start based on data availability with timeout logic.
351
+
352
+ Two-phase approach:
353
+ - Phase 1 (0-DATA_READY_TIMEOUT_SECONDS): Wait for ALL instruments to have data
354
+ - Phase 2 (after timeout): Wait for at least 1 instrument to have data
355
+
356
+ Returns:
357
+ bool: True if strategy can start, False if still waiting
348
358
  """
349
- return all(instrument in self._updated_instruments for instrument in self._context.instruments)
359
+ total_instruments = len(self._context.instruments)
360
+
361
+ # Handle edge case: no instruments
362
+ if total_instruments == 0:
363
+ return True
364
+
365
+ ready_instruments = len(self._updated_instruments)
366
+
367
+ # Record start time on first call
368
+ if self._data_ready_start_time is None:
369
+ self._data_ready_start_time = self._time_provider.time()
370
+
371
+ # Phase 1: Try to get all instruments ready within timeout
372
+ elapsed_time_seconds = (self._time_provider.time() - self._data_ready_start_time) / 1e9
373
+
374
+ if elapsed_time_seconds <= self.DATA_READY_TIMEOUT_SECONDS:
375
+ # Within timeout period - wait for ALL instruments
376
+ if ready_instruments == total_instruments:
377
+ logger.info(f"All {total_instruments} instruments have data - strategy ready to start")
378
+ return True
379
+ else:
380
+ # Log periodic status during Phase 1
381
+ if int(elapsed_time_seconds) % 10 == 0 and elapsed_time_seconds > 0: # Log every 10 seconds
382
+ missing_instruments = set(self._context.instruments) - self._updated_instruments
383
+ missing_symbols = [inst.symbol for inst in missing_instruments]
384
+ logger.info(
385
+ f"Phase 1: Waiting for all instruments ({ready_instruments}/{total_instruments} ready). "
386
+ f"Missing: {missing_symbols}. Timeout in {self.DATA_READY_TIMEOUT_SECONDS - elapsed_time_seconds}s"
387
+ )
388
+ return False
389
+ else:
390
+ # Phase 2: After timeout - need at least 1 instrument
391
+ if ready_instruments >= 1:
392
+ missing_instruments = set(self._context.instruments) - self._updated_instruments
393
+ missing_symbols = [inst.symbol for inst in missing_instruments]
394
+ logger.info(
395
+ f"Starting strategy with {ready_instruments}/{total_instruments} instruments ready. Missing: {missing_symbols}"
396
+ )
397
+ return True
398
+ else:
399
+ return False
350
400
 
351
401
  def __update_base_data(
352
402
  self, instrument: Instrument, event_type: str, data: Timestamped, is_historical: bool = False
@@ -114,11 +114,12 @@ class UniverseManager(IUniverseManager):
114
114
  self._removal_queue.pop(instr)
115
115
 
116
116
  def add_instruments(self, instruments: list[Instrument]):
117
- self.__do_add_instruments(instruments)
117
+ to_add = list(set([instr for instr in instruments if instr not in self._instruments]))
118
+ self.__do_add_instruments(to_add)
118
119
  self.__cleanup_removal_queue(instruments)
119
- self._strategy.on_universe_change(self._context, instruments, [])
120
+ self._strategy.on_universe_change(self._context, to_add, [])
120
121
  self._subscription_manager.commit()
121
- self._instruments.update(instruments)
122
+ self._instruments.update(to_add)
122
123
 
123
124
  def remove_instruments(
124
125
  self,
qubx/data/composite.py CHANGED
@@ -71,12 +71,15 @@ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
71
71
  return self
72
72
 
73
73
  def _build_initial_iteration_seq(self):
74
- _init_seq = {k: self._time_func(self._buffers[k][-1]) for k in self._keys}
74
+ _init_seq = {k: self._time_func(self._buffers[k][-1]) for k in self._keys if self._buffers[k]}
75
75
  _init_seq = dict(sorted(_init_seq.items(), key=lambda item: item[1]))
76
76
  self._keys = deque(_init_seq.keys())
77
77
 
78
78
  def _load_next_chunk_to_buffer(self, index: str) -> list[Timestamped]:
79
- return list(reversed(next(self._iterators[index])))
79
+ try:
80
+ return list(reversed(next(self._iterators[index])))
81
+ except StopIteration:
82
+ return []
80
83
 
81
84
  def _remove_iterator(self, key: str):
82
85
  self._buffers.pop(key)
@@ -95,6 +98,9 @@ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
95
98
  Returns:
96
99
  Timestamped: The most recent timestamped data element from the buffer.
97
100
  """
101
+ if not self._buffers[k]:
102
+ raise StopIteration
103
+
98
104
  v = (data := self._buffers[k]).pop()
99
105
  if not data:
100
106
  try:
@@ -154,6 +160,9 @@ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
154
160
  _min_t = math.inf
155
161
  _min_k = self._keys[0]
156
162
  for i in self._keys:
163
+ if not self._buffers[i]:
164
+ continue
165
+
157
166
  _x = self._buffers[i][-1]
158
167
  if self._time_func(_x) < _min_t:
159
168
  _min_t = self._time_func(_x)
qubx/data/readers.py CHANGED
@@ -1469,6 +1469,11 @@ class QuestDBConnector(DataReader):
1469
1469
  # Use efficient chunking with multiple smaller queries
1470
1470
  def _iter_efficient_chunks():
1471
1471
  time_windows = _calculate_time_windows_for_chunking(start, end, effective_timeframe, chunksize)
1472
+ if self._connection is None:
1473
+ self._connect()
1474
+ if self._connection is None:
1475
+ raise ConnectionError("Failed to connect to QuestDB")
1476
+
1472
1477
  _cursor = self._connection.cursor() # type: ignore
1473
1478
 
1474
1479
  try:
qubx/emitters/base.py CHANGED
@@ -207,6 +207,6 @@ class BaseMetricEmitter(IMetricEmitter):
207
207
  elapsed = current_time - self._last_emission_time
208
208
 
209
209
  if elapsed >= self._stats_interval:
210
- logger.debug(f"[{self.__class__.__name__}] Emitting metrics at {current_time}")
210
+ # logger.debug(f"[{self.__class__.__name__}] Emitting metrics at {current_time}")
211
211
  self.emit_strategy_stats(context)
212
212
  self._last_emission_time = current_time
qubx/loggers/__init__.py CHANGED
@@ -5,9 +5,9 @@ This module provides implementations for logs writing, like csv writer or mongod
5
5
  """
6
6
 
7
7
  from qubx.loggers.csv import CsvFileLogsWriter
8
+ from qubx.loggers.factory import create_logs_writer
8
9
  from qubx.loggers.inmemory import InMemoryLogsWriter
9
10
  from qubx.loggers.mongo import MongoDBLogsWriter
10
- from qubx.loggers.factory import create_logs_writer
11
11
 
12
12
  __all__ = [
13
13
  "CsvFileLogsWriter",
qubx/loggers/csv.py CHANGED
@@ -6,7 +6,8 @@ from multiprocessing.pool import ThreadPool
6
6
 
7
7
  from qubx import logger
8
8
  from qubx.core.loggers import LogsWriter
9
- from qubx.utils.misc import makedirs
9
+ from qubx.utils.misc import makedirs
10
+
10
11
 
11
12
  class CsvFileLogsWriter(LogsWriter):
12
13
  """
@@ -97,4 +98,3 @@ class CsvFileLogsWriter(LogsWriter):
97
98
  self._sig_file_.close()
98
99
  self.pool.close()
99
100
  self.pool.join()
100
-
qubx/loggers/factory.py CHANGED
@@ -1,19 +1,19 @@
1
1
  import inspect
2
-
3
2
  from typing import Type
4
3
 
5
4
  from qubx.core.loggers import LogsWriter
6
5
  from qubx.loggers.csv import CsvFileLogsWriter
7
- from qubx.loggers.mongo import MongoDBLogsWriter
8
6
  from qubx.loggers.inmemory import InMemoryLogsWriter
7
+ from qubx.loggers.mongo import MongoDBLogsWriter
9
8
 
10
9
  # Registry of logs writer types
11
10
  LOGS_WRITER_REGISTRY: dict[str, Type[LogsWriter]] = {
12
11
  "CsvFileLogsWriter": CsvFileLogsWriter,
13
12
  "MongoDBLogsWriter": MongoDBLogsWriter,
14
- "InMemoryLogsWriter": InMemoryLogsWriter
13
+ "InMemoryLogsWriter": InMemoryLogsWriter,
15
14
  }
16
15
 
16
+
17
17
  def create_logs_writer(log_writer_type: str, parameters: dict | None = None) -> LogsWriter:
18
18
  """
19
19
  Create a logs writer based on configuration.
@@ -30,8 +30,7 @@ def create_logs_writer(log_writer_type: str, parameters: dict | None = None) ->
30
30
  """
31
31
  if log_writer_type not in LOGS_WRITER_REGISTRY:
32
32
  raise ValueError(
33
- f"Unknown logs writer type: {log_writer_type}. "
34
- f"Available types: {', '.join(LOGS_WRITER_REGISTRY.keys())}"
33
+ f"Unknown logs writer type: {log_writer_type}. Available types: {', '.join(LOGS_WRITER_REGISTRY.keys())}"
35
34
  )
36
35
 
37
36
  logs_writer_class = LOGS_WRITER_REGISTRY[log_writer_type]
@@ -52,4 +51,4 @@ def register_logs_writer(log_writer_type: str, logs_witer_class: Type[LogsWriter
52
51
  log_writer_type: The name of the logs writer type.
53
52
  logs_witer_class: The logs writer class to register.
54
53
  """
55
- LOGS_WRITER_REGISTRY[log_writer_type] = logs_witer_class
54
+ LOGS_WRITER_REGISTRY[log_writer_type] = logs_witer_class
qubx/loggers/inmemory.py CHANGED
@@ -1,15 +1,17 @@
1
- import pandas as pd
1
+ from typing import Any
2
2
 
3
- from typing import Any, Dict, List
3
+ import pandas as pd
4
4
 
5
+ from qubx import logger
5
6
  from qubx.core.loggers import LogsWriter
6
7
  from qubx.core.metrics import split_cumulative_pnl
7
8
  from qubx.pandaz.utils import scols
8
9
 
10
+
9
11
  class InMemoryLogsWriter(LogsWriter):
10
- _portfolio: List
11
- _execs: List
12
- _signals: List
12
+ _portfolio: list[dict[str, Any]]
13
+ _execs: list[dict[str, Any]]
14
+ _signals: list[dict[str, Any]]
13
15
 
14
16
  def __init__(self, account_id: str, strategy_id: str, run_id: str) -> None:
15
17
  super().__init__(account_id, strategy_id, run_id)
@@ -17,7 +19,7 @@ class InMemoryLogsWriter(LogsWriter):
17
19
  self._execs = []
18
20
  self._signals = []
19
21
 
20
- def write_data(self, log_type: str, data: List[Dict[str, Any]]):
22
+ def write_data(self, log_type: str, data: list[dict[str, Any]]):
21
23
  if len(data) > 0:
22
24
  if log_type == "portfolio":
23
25
  self._portfolio.extend(data)
@@ -27,31 +29,35 @@ class InMemoryLogsWriter(LogsWriter):
27
29
  self._signals.extend(data)
28
30
 
29
31
  def get_portfolio(self, as_plain_dataframe=True) -> pd.DataFrame:
30
- pfl = pd.DataFrame.from_records(self._portfolio, index="timestamp")
31
- pfl.index = pd.DatetimeIndex(pfl.index)
32
- if as_plain_dataframe:
33
- # - convert to Qube presentation (TODO: temporary)
34
- pis = []
35
- for s in set(pfl["symbol"]):
36
- pi = pfl[pfl["symbol"] == s]
37
- pi = pi.drop(columns=["symbol", "realized_pnl_quoted", "current_price", "exchange_time"])
38
- pi = pi.rename(
39
- {
40
- "pnl_quoted": "PnL",
41
- "quantity": "Pos",
42
- "avg_position_price": "Price",
43
- "market_value_quoted": "Value",
44
- "commissions_quoted": "Commissions",
45
- },
46
- axis=1,
47
- )
48
- # We want to convert the value to just price * quantity
49
- # in reality value of perps is just the unrealized pnl but
50
- # it's not important after simulation for metric calculations
51
- pi["Value"] = pi["Pos"] * pi["Price"] + pi["Value"]
52
- pis.append(pi.rename(lambda x: s + "_" + x, axis=1))
53
- return split_cumulative_pnl(scols(*pis))
54
- return pfl
32
+ try:
33
+ pfl = pd.DataFrame.from_records(self._portfolio, index="timestamp")
34
+ pfl.index = pd.DatetimeIndex(pfl.index)
35
+ if as_plain_dataframe:
36
+ # - convert to Qube presentation (TODO: temporary)
37
+ pis = []
38
+ for s in set(pfl["symbol"]):
39
+ pi = pfl[pfl["symbol"] == s]
40
+ pi = pi.drop(columns=["symbol", "realized_pnl_quoted", "current_price", "exchange_time"])
41
+ pi = pi.rename(
42
+ {
43
+ "pnl_quoted": "PnL",
44
+ "quantity": "Pos",
45
+ "avg_position_price": "Price",
46
+ "market_value_quoted": "Value",
47
+ "commissions_quoted": "Commissions",
48
+ },
49
+ axis=1,
50
+ )
51
+ # We want to convert the value to just price * quantity
52
+ # in reality value of perps is just the unrealized pnl but
53
+ # it's not important after simulation for metric calculations
54
+ pi["Value"] = pi["Pos"] * pi["Price"] + pi["Value"]
55
+ pis.append(pi.rename(lambda x: s + "_" + x, axis=1))
56
+ return split_cumulative_pnl(scols(*pis))
57
+ return pfl
58
+ except Exception as e:
59
+ logger.error(f":: Error getting portfolio: {e} ::\n{self._portfolio}")
60
+ return pd.DataFrame()
55
61
 
56
62
  def get_executions(self) -> pd.DataFrame:
57
63
  p = pd.DataFrame()
@@ -65,4 +71,4 @@ class InMemoryLogsWriter(LogsWriter):
65
71
  if self._signals:
66
72
  p = pd.DataFrame.from_records(self._signals, index="timestamp")
67
73
  p.index = pd.DatetimeIndex(p.index)
68
- return p
74
+ return p
qubx/loggers/mongo.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from datetime import datetime
2
2
  from multiprocessing.pool import ThreadPool
3
+ from typing import Any
4
+
3
5
  from pymongo import MongoClient
4
- from typing import Any, Dict, List
5
6
 
6
7
  from qubx.core.loggers import LogsWriter
7
8
 
@@ -31,25 +32,13 @@ class MongoDBLogsWriter(LogsWriter):
31
32
  self.collection_name_prefix = collection_name_prefix
32
33
 
33
34
  # Ensure TTL index exists on the 'timestamp' field
34
- self.db[f"{collection_name_prefix}_positions"].create_index(
35
- "timestamp", expireAfterSeconds=ttl_seconds
36
- )
37
- self.db[f"{collection_name_prefix}_portfolio"].create_index(
38
- "timestamp", expireAfterSeconds=ttl_seconds
39
- )
40
- self.db[f"{collection_name_prefix}_executions"].create_index(
41
- "timestamp", expireAfterSeconds=ttl_seconds
42
- )
43
- self.db[f"{collection_name_prefix}_signals"].create_index(
44
- "timestamp", expireAfterSeconds=ttl_seconds
45
- )
46
- self.db[f"{collection_name_prefix}_balance"].create_index(
47
- "timestamp", expireAfterSeconds=ttl_seconds
48
- )
49
-
50
- def _attach_metadata(
51
- self, data: List[Dict[str, Any]], log_type: str
52
- ) -> List[Dict[str, Any]]:
35
+ self.db[f"{collection_name_prefix}_positions"].create_index("timestamp", expireAfterSeconds=ttl_seconds)
36
+ self.db[f"{collection_name_prefix}_portfolio"].create_index("timestamp", expireAfterSeconds=ttl_seconds)
37
+ self.db[f"{collection_name_prefix}_executions"].create_index("timestamp", expireAfterSeconds=ttl_seconds)
38
+ self.db[f"{collection_name_prefix}_signals"].create_index("timestamp", expireAfterSeconds=ttl_seconds)
39
+ self.db[f"{collection_name_prefix}_balance"].create_index("timestamp", expireAfterSeconds=ttl_seconds)
40
+
41
+ def _attach_metadata(self, data: list[dict[str, Any]], log_type: str) -> list[dict[str, Any]]:
53
42
  now = datetime.utcnow()
54
43
  return [
55
44
  {
@@ -62,19 +51,25 @@ class MongoDBLogsWriter(LogsWriter):
62
51
  }
63
52
  for d in data
64
53
  ]
65
-
66
- def _do_write(self, log_type: str, data: List[Dict[str, Any]]):
54
+
55
+ def _do_write(self, log_type: str, data: list[dict[str, Any]]):
67
56
  docs = self._attach_metadata(data, log_type)
68
57
  self.db[f"{self.collection_name_prefix}_{log_type}"].insert_many(docs)
69
58
 
70
- def write_data(self, log_type: str, data: List[Dict[str, Any]]):
59
+ def write_data(self, log_type: str, data: list[dict[str, Any]]):
71
60
  if len(data) > 0:
72
- self.pool.apply_async(self._do_write, (log_type, data,))
73
-
61
+ self.pool.apply_async(
62
+ self._do_write,
63
+ (
64
+ log_type,
65
+ data,
66
+ ),
67
+ )
68
+
74
69
  def flush_data(self):
75
70
  pass
76
-
71
+
77
72
  def close(self):
78
73
  self.pool.close()
79
74
  self.pool.join()
80
- self.client.close()
75
+ self.client.close()
@@ -5,6 +5,7 @@ This module provides a Slack implementation of IStrategyLifecycleNotifier.
5
5
  """
6
6
 
7
7
  import datetime
8
+ import threading
8
9
  from concurrent.futures import ThreadPoolExecutor
9
10
  from typing import Any
10
11
 
@@ -52,6 +53,9 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
52
53
  self._emoji_error = emoji_error
53
54
  self._throttler = throttler if throttler is not None else NoThrottling()
54
55
 
56
+ # Add a lock for thread-safe throttling operations
57
+ self._throttler_lock = threading.Lock()
58
+
55
59
  self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="slack_notifier")
56
60
 
57
61
  logger.info(f"[SlackLifecycleNotifier] Initialized for environment '{environment}'")
@@ -75,10 +79,16 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
75
79
  throttle_key: Optional key for throttling (if None, no throttling is applied)
76
80
  """
77
81
  try:
78
- # Check if the message should be throttled
79
- if throttle_key is not None and not self._throttler.should_send(throttle_key):
80
- logger.debug(f"[SlackLifecycleNotifier] Throttled message with key '{throttle_key}': {message}")
81
- return
82
+ # Thread-safe throttling check and registration
83
+ if throttle_key is not None:
84
+ with self._throttler_lock:
85
+ if not self._throttler.should_send(throttle_key):
86
+ logger.debug(f"[SlackLifecycleNotifier] Throttled message with key '{throttle_key}': {message}")
87
+ return
88
+ # Immediately register that we're about to send this message
89
+ # This prevents race conditions where multiple threads check should_send
90
+ # before any of them call register_sent
91
+ self._throttler.register_sent(throttle_key)
82
92
 
83
93
  # Submit the task to the executor
84
94
  self._executor.submit(self._post_to_slack_impl, message, emoji, color, metadata, throttle_key)
@@ -132,10 +142,6 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
132
142
  response = requests.post(self._webhook_url, json=data)
133
143
  response.raise_for_status()
134
144
 
135
- # Register that we sent the message (for throttling)
136
- if throttle_key is not None:
137
- self._throttler.register_sent(throttle_key)
138
-
139
145
  logger.debug(f"[SlackLifecycleNotifier] Successfully posted message: {message}")
140
146
  return True
141
147
  except requests.RequestException as e:
@@ -55,7 +55,7 @@ class StateResolver:
55
55
  elif abs(live_qty) > abs(sim_qty) and abs(live_qty) > instrument.lot_size:
56
56
  qty_diff = sim_qty - live_qty
57
57
  logger.info(
58
- f"Reducing position for {instrument.symbol}: {live_qty} -> {sim_qty} (diff: {qty_diff})"
58
+ f"Reducing position for {instrument.symbol}: {live_qty} -> {sim_qty} (diff: {qty_diff:.4f})"
59
59
  )
60
60
  ctx.trade(instrument, qty_diff)
61
61
 
@@ -123,7 +123,7 @@ class StateResolver:
123
123
  # Only trade if there's a difference
124
124
  if abs(qty_diff) > instrument.lot_size:
125
125
  logger.info(
126
- f"Syncing position for {instrument.symbol}: {live_qty} -> {sim_pos.quantity} (diff: {qty_diff})"
126
+ f"Syncing position for {instrument.symbol}: {live_qty} -> {sim_pos.quantity} (diff: {qty_diff:.4f})"
127
127
  )
128
128
  ctx.trade(instrument, qty_diff)
129
129
 
qubx/restorers/signal.py CHANGED
@@ -208,31 +208,10 @@ class MongoDBSignalRestorer(ISignalRestorer):
208
208
  A dictionary mapping instruments to lists of signals.
209
209
  """
210
210
  try:
211
- now = datetime.utcnow()
212
- lookup_range = now - timedelta(days=30)
213
- base_match = {
214
- "log_type": "signals",
215
- "strategy_name": self.strategy_name,
216
- "timestamp": {"$gte": lookup_range}
217
- }
218
-
219
- latest_run_doc = (
220
- self.collection.find(base_match, {"run_id": 1, "timestamp": 1})
221
- .sort("timestamp", -1)
222
- .limit(1)
223
- )
224
-
225
- latest_run = next(latest_run_doc, None)
226
- if not latest_run:
227
- logger.warning("No signal logs found for given filters.")
228
- return {}
229
-
230
- latest_run_id = latest_run["run_id"]
231
-
232
- logger.info(f"Restoring signals from MongoDB for run_id: {latest_run_id}")
211
+ logger.info(f"Restoring latest 20 signals per symbol from MongoDB")
233
212
 
234
213
  pipeline = [
235
- {"$match": {"log_type": "signals", "strategy_name": self.strategy_name, "run_id": latest_run_id}},
214
+ {"$match": {"log_type": "signals", "strategy_name": self.strategy_name}},
236
215
  {"$sort": {"timestamp": -1}},
237
216
  {
238
217
  "$group": {
@@ -103,6 +103,7 @@ class SimulationConfig(BaseModel):
103
103
  n_jobs: int | None = None
104
104
  variate: dict = Field(default_factory=dict)
105
105
  debug: str | None = None
106
+ run_separate_instruments: bool = False
106
107
 
107
108
 
108
109
  class StrategyConfig(BaseModel):
@@ -44,13 +44,16 @@ def construct_reader(reader_config: ReaderConfig | None) -> DataReader | None:
44
44
  raise
45
45
 
46
46
 
47
- def create_metric_emitters(emission_config: EmissionConfig, strategy_name: str) -> IMetricEmitter | None:
47
+ def create_metric_emitters(
48
+ emission_config: EmissionConfig, strategy_name: str, run_id: str | None = None
49
+ ) -> IMetricEmitter | None:
48
50
  """
49
51
  Create metric emitters from the configuration.
50
52
 
51
53
  Args:
52
54
  emission_config: Configuration for metric emission
53
55
  strategy_name: Name of the strategy to be included in tags
56
+ run_id: Optional run ID to be included in tags
54
57
 
55
58
  Returns:
56
59
  IMetricEmitter or None if no metric emitters are configured
@@ -97,6 +100,8 @@ def create_metric_emitters(emission_config: EmissionConfig, strategy_name: str)
97
100
  tags[k] = resolve_env_vars(v)
98
101
 
99
102
  tags["strategy"] = strategy_name
103
+ if run_id is not None:
104
+ tags["run_id"] = run_id
100
105
 
101
106
  # Add tags if the emitter supports it
102
107
  if "tags" in inspect.signature(emitter_class).parameters:
@@ -181,9 +186,9 @@ def create_data_type_readers(readers_configs: list[TypedReaderConfig] | None) ->
181
186
 
182
187
 
183
188
  def create_exporters(
184
- exporters: list[ExporterConfig] | None,
185
- strategy_name: str,
186
- account: Optional[IAccountViewer] = None,
189
+ exporters: list[ExporterConfig] | None,
190
+ strategy_name: str,
191
+ account: Optional[IAccountViewer] = None,
187
192
  ) -> ITradeDataExport | None:
188
193
  """
189
194
  Create exporters from the configuration.
@@ -293,7 +298,9 @@ def create_lifecycle_notifiers(
293
298
  params[key] = resolve_env_vars(value)
294
299
 
295
300
  # Create throttler if configured or use default TimeWindowThrottler
296
- if "SlackLifecycleNotifier" in notifier_class_name and "throttler" not in params:
301
+ if "SlackLifecycleNotifier" in notifier_class_name and (
302
+ "throttle" not in params or params["throttle"] is None
303
+ ):
297
304
  # Import here to avoid circular imports
298
305
  from qubx.notifications.throttler import TimeWindowThrottler
299
306
 
@@ -28,6 +28,7 @@ from qubx.core.basics import (
28
28
  CtrlChannel,
29
29
  Instrument,
30
30
  LiveTimeProvider,
31
+ Position,
31
32
  RestoredState,
32
33
  TransactionCostsCalculator,
33
34
  )
@@ -263,6 +264,9 @@ def create_strategy_context(
263
264
  stg_name = _get_strategy_name(config)
264
265
  _run_mode = "paper" if paper else "live"
265
266
 
267
+ # Generate run_id once to be shared between logging and metric emissions
268
+ run_id = f"{socket.gethostname()}-{str(int(time.time() * 10**9))}"
269
+
266
270
  if isinstance(config.strategy, list):
267
271
  _strategy_class = reduce(lambda x, y: x + y, [class_import(x) for x in config.strategy])
268
272
  elif isinstance(config.strategy, str):
@@ -270,12 +274,12 @@ def create_strategy_context(
270
274
  else:
271
275
  _strategy_class = config.strategy
272
276
 
273
- _logging = _setup_strategy_logging(stg_name, config.live.logging, simulated_formatter, no_color)
277
+ _logging = _setup_strategy_logging(stg_name, config.live.logging, simulated_formatter, run_id)
274
278
 
275
279
  _aux_reader = construct_reader(config.aux) if config.aux else None
276
280
 
277
- # Create metric emitters
278
- _metric_emitter = create_metric_emitters(config.live.emission, stg_name) if config.live.emission else None
281
+ # Create metric emitters with run_id as a tag
282
+ _metric_emitter = create_metric_emitters(config.live.emission, stg_name, run_id) if config.live.emission else None
279
283
 
280
284
  # Create lifecycle notifiers
281
285
  _lifecycle_notifier = create_lifecycle_notifiers(config.live.notifiers, stg_name) if config.live.notifiers else None
@@ -387,7 +391,10 @@ def _get_strategy_name(cfg: StrategyConfig) -> str:
387
391
 
388
392
 
389
393
  def _setup_strategy_logging(
390
- stg_name: str, log_config: LoggingConfig, simulated_formatter: SimulatedLogFormatter, no_color: bool = False
394
+ stg_name: str,
395
+ log_config: LoggingConfig,
396
+ simulated_formatter: SimulatedLogFormatter,
397
+ run_id: str,
391
398
  ) -> StrategyLogging:
392
399
  if not hasattr(log_config, "args") or not isinstance(log_config.args, dict):
393
400
  log_config.args = {}
@@ -402,8 +409,6 @@ def _setup_strategy_logging(
402
409
  level=QubxLogConfig.get_log_level(),
403
410
  )
404
411
 
405
- run_id = f"{socket.gethostname()}-{str(int(time.time() * 10**9))}"
406
-
407
412
  _log_writer_name = log_config.logger
408
413
 
409
414
  logger.debug(f"Setup <g>{_log_writer_name}</g> logger...")
@@ -691,6 +696,14 @@ def _run_warmup(
691
696
  if o.instrument in _instruments:
692
697
  instrument_to_orders[o.instrument].append(o)
693
698
 
699
+ # - find instruments with nonzero positions from restored state and add them to the context
700
+ if restored_state is not None:
701
+ restored_positions = {k: p for k, p in restored_state.positions.items() if p.is_open()}
702
+ # - if there is no warmup position for a restored position, then create a new zero position
703
+ for pos in restored_positions.values():
704
+ if pos.instrument not in _positions:
705
+ _positions[pos.instrument] = Position(pos.instrument)
706
+
694
707
  # - set the warmup positions and orders
695
708
  ctx.set_warmup_positions(_positions)
696
709
  ctx.set_warmup_orders(instrument_to_orders)
@@ -737,6 +750,9 @@ def simulate_strategy(
737
750
  if cfg.simulation is None:
738
751
  raise ValueError("Simulation configuration is required")
739
752
 
753
+ if cfg.simulation.run_separate_instruments and cfg.simulation.variate:
754
+ raise ValueError("Run separate instruments is not supported with variate")
755
+
740
756
  stg = cfg.strategy
741
757
  simulation_name = config_file.stem
742
758
  _v_id = pd.Timestamp("now").strftime("%Y%m%d%H%M%S")
@@ -759,6 +775,11 @@ def simulate_strategy(
759
775
  for a, c in cond.items():
760
776
  conditions.append(dict2lambda(a, c))
761
777
 
778
+ # - if a parameter is of type list, then transform it to list of lists to avoid invalid variation
779
+ for k, v in cfg.parameters.items():
780
+ if isinstance(v, list):
781
+ cfg.parameters[k] = [v]
782
+
762
783
  experiments = variate(stg_cls, **(cfg.parameters | cfg.simulation.variate), conditions=conditions)
763
784
  experiments = {f"{simulation_name}.{_v_id}.[{k}]": v for k, v in experiments.items()}
764
785
  print(f"Parameters variation is configured. There are {len(experiments)} simulations to run.")
@@ -790,10 +811,17 @@ def simulate_strategy(
790
811
  sim_params["stop"] = stop
791
812
  logger.info(f"Stop date set to {stop}")
792
813
 
814
+ if cfg.simulation.n_jobs is not None:
815
+ sim_params["n_jobs"] = cfg.simulation.n_jobs
816
+
793
817
  # - check for aux_data parameter
794
818
  if cfg.aux is not None:
795
819
  sim_params["aux_data"] = construct_reader(cfg.aux)
796
820
 
821
+ # - add run_separate_instruments parameter
822
+ if cfg.simulation.run_separate_instruments:
823
+ sim_params["run_separate_instruments"] = True
824
+
797
825
  # - run simulation
798
826
  print(f" > Run simulation for [{red(simulation_name)}] ::: {sim_params['start']} - {sim_params['stop']}")
799
827
  sim_params["n_jobs"] = sim_params.get("n_jobs", _n_jobs)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.49
3
+ Version: 0.6.52
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -3,15 +3,15 @@ qubx/_nb_magic.py,sha256=G3LkaX_-gN5Js6xl7rjaahSs_u3dVPDRCZW0IIxPCb0,3051
3
3
  qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
4
4
  qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,3048
5
5
  qubx/backtester/broker.py,sha256=JMasxycLqCT99NxN50uyQ1uxtpHYL0wpp4sJ3hB6v2M,2688
6
- qubx/backtester/data.py,sha256=De2ioNv3Zh-cCGakzK0igb2caDcqbibZ_tsYmF7sTTQ,6601
6
+ qubx/backtester/data.py,sha256=B1VHioLqBwA6ZnEgTn5Gere1vfOw0KvyFjGwm4vlByQ,6675
7
7
  qubx/backtester/management.py,sha256=HuyzFsBPgR7j-ei78Ngcx34CeSn65c9atmaii1aTsYg,14900
8
8
  qubx/backtester/ome.py,sha256=BC8EuJkPTiGbl8HliHehVzwdD0OSDlR04g6RVA66FQE,18614
9
9
  qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
10
10
  qubx/backtester/runner.py,sha256=TnNM0t8PgBE_gnCOZZTIOc28a3RqtXmp2Xj4Gq5j6bo,20504
11
11
  qubx/backtester/simulated_data.py,sha256=niujaMRj__jf4IyzCZrSBR5ZoH1VUbvsZHSewHftdmI,17240
12
12
  qubx/backtester/simulated_exchange.py,sha256=Xg0yv21gq4q9CeCeZoupcenNEBORrxpb93ONZEGL2xk,8076
13
- qubx/backtester/simulator.py,sha256=cSbW42X-YlAutZlOQ3Y4mAJWXr_1WomYprtWZVMe3Uk,9225
14
- qubx/backtester/utils.py,sha256=dDaS7Wrc_xZXWuAqKDql7L3NRMvUkTxpuuAwS0SYA1c,32721
13
+ qubx/backtester/simulator.py,sha256=rBDV6Ftq8ProK-VOkewtdhAcM0-kkvv3KTszB-uHdrY,11154
14
+ qubx/backtester/utils.py,sha256=E7_MQhDGXXvFyjt0VSFZiQrNqP02fBml9AC93b6WxZs,34760
15
15
  qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  qubx/cli/commands.py,sha256=MdiBGb8Z3mAFDq44Dn4EuSQRk2aArIrfo1GJ-VwgeQQ,8197
17
17
  qubx/cli/deploy.py,sha256=pQ9FPOsywDyy8jOjLfrgYTTkKQ-MCixCzbgsG68Q3_0,8319
@@ -44,31 +44,31 @@ qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
44
44
  qubx/core/helpers.py,sha256=m7JrZaBckXHb4zjhKpdCbxFe3kfda-SCNLfagyq7Ve4,19158
45
45
  qubx/core/initializer.py,sha256=YgTBs5LpIk6ZFdmMD8zCnJnVNcMh1oeYvt157jhwyMg,4242
46
46
  qubx/core/interfaces.py,sha256=gwitvNSni2zUWdpcRyqAdw9n3l1-Z6TBvl9D--hIqgM,59104
47
- qubx/core/loggers.py,sha256=0g33jfipGFShSMrXBoYVzL0GfTzI36mwBJqHNUHmhdo,13342
47
+ qubx/core/loggers.py,sha256=85Xgt1-Hh-2gAlJez3TxTHr32KSWYNqNhmbeWZwhw0o,13340
48
48
  qubx/core/lookups.py,sha256=aEuyZqd_N4cQ-oHz3coEHcdX9Yb0cP5-NwDuj-DQyNk,19477
49
49
  qubx/core/metrics.py,sha256=74xIecCvlxVXl0gy0JvgjJ2X5gg-RMmVZw9hQikkHE0,60269
50
50
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
51
51
  qubx/core/mixins/market.py,sha256=lBappEimPhIuI0vmUvwVlIztkYjlEjJBpP-AdpfudII,3948
52
- qubx/core/mixins/processing.py,sha256=cmjD0PcQv3gFP6oILfNgdNgw7Tez0fUZu_nFn6680VI,24979
52
+ qubx/core/mixins/processing.py,sha256=VEaK6ZjXTa8jvavj_VpCYfGvLFTHpNoL1AKdRAeear8,27394
53
53
  qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
54
54
  qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
55
- qubx/core/mixins/universe.py,sha256=V8Hs9I9gxSCftWlaVepjlIyAji7R_99OyjlincaOBpU,9758
56
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=qr_Kp48TninYcwnu3KCh6A09j-IBn9pChO_AO2IfKb0,978280
55
+ qubx/core/mixins/universe.py,sha256=tsMpBriLHwK9lAVYvIrO94EIx8_ETSXUlzxN_sDOsL8,9838
56
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=SpozS5G2UEoeCbgVsaorjvwbKo5U4d5EqxP3pSktzyM,978280
57
57
  qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
58
58
  qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
59
59
  qubx/core/series.pyx,sha256=7cM3zZThW59waHiYcZmMxvYj-HYD7Ej_l7nKA4emPjE,46477
60
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=75SE7kvxEyS7dtCgUQ5_mdOs2uXBdoo3APlhRGldQFg,86568
60
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=c3ZAL_JGRFpOPUlSITmuQ_jjxv4bA1CQAV9c13wtUfI,86568
61
61
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
62
62
  qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
63
63
  qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
64
- qubx/data/composite.py,sha256=bcFJzIzR2-IfVW8Ot3cUibKS8smnmRbHipd8ztIuScs,18015
64
+ qubx/data/composite.py,sha256=l1FjJ2RnX7pxhah-cDBw7CWQQvwqKBCANXKiHRrZBzc,18233
65
65
  qubx/data/helpers.py,sha256=VcXBl1kfWzAOqrjadKrP9WemGjJIB0q3xascbesErh4,16268
66
66
  qubx/data/hft.py,sha256=be7AwzTOjqqCENn0ClrZoHDyKv3SFG66IyTp8QadHlM,33687
67
- qubx/data/readers.py,sha256=nSmkH6X-O1uuDVO3dVC47k46etIO-SLvGp0lGfI8pYE,65994
67
+ qubx/data/readers.py,sha256=g3hSkyKdMAVziMCgcaZadsukidECaLwHyIEtArSVDSc,66203
68
68
  qubx/data/registry.py,sha256=45mjy5maBSO6cf-0zfIRRDs8b0VDW7wHSPn43aRjv-o,3883
69
69
  qubx/data/tardis.py,sha256=O-zglpusmO6vCY3arSOgH6KUbkfPajSAIQfMKlVmh_E,33878
70
70
  qubx/emitters/__init__.py,sha256=tpJ9OoW-gycTBXGJ0647tT8-dVBmq23T2wMX_kmk3nM,565
71
- qubx/emitters/base.py,sha256=eo5yD2402oNVndhNwFspIzB-gZMrZtLRx63JvivdC8Q,7974
71
+ qubx/emitters/base.py,sha256=os69vCs00eRELZn-1TYR7MZXJbnrFOfeW4UEtMLCphw,7976
72
72
  qubx/emitters/composite.py,sha256=8DsPIUtaJ95Oww9QTVVB6LR7Wcb6TJ-c1jIHMGuttz4,2784
73
73
  qubx/emitters/csv.py,sha256=lWl6sP0ke0j6kVlEbQsy11vSOHFudYHjWS9iPbq6kmo,3067
74
74
  qubx/emitters/prometheus.py,sha256=g2hgcV_G77fWVEXtoGJTUs4JLkB2FQXFzVY_x_sEBfc,8100
@@ -90,16 +90,16 @@ qubx/features/utils.py,sha256=5wMlfH4x1dUh00dxvtnHhSiHeRaiod4VMTcmgm-o_wA,264
90
90
  qubx/gathering/simplest.py,sha256=24SIjsCfutuTinSW5zSkPHGJvl-vnyhe3FAX3quUx4E,4011
91
91
  qubx/health/__init__.py,sha256=ThJTgf-CPD5tMU_emqANpnE6oXfUmzyyugfbDfzeVB0,111
92
92
  qubx/health/base.py,sha256=FmpZ7l_L-wJ8JCQ42uRjng3hAbhfmAf3nIUc4vSs9cI,27622
93
- qubx/loggers/__init__.py,sha256=HY26weGv_qI-i5NrvXepGCwXENOIeORW9uhvyFMVMcM,443
94
- qubx/loggers/csv.py,sha256=95JLFz2yxo_OjSG21cmWVYZP1XuN3V0FXNPUB_q6ucY,3791
95
- qubx/loggers/factory.py,sha256=09ahIIQU_59vI4OO1rKYLAHgBbZ2506wv0FqYie_Rb0,1805
96
- qubx/loggers/inmemory.py,sha256=49Y-jsRxDzBLWQdQMIKjVTvnx_79EbjFpHwj3v8Mgno,2642
97
- qubx/loggers/mongo.py,sha256=dOEpCcIxT6O9MgpK2srpzxyuto6QaQgTxMK0WcEIR70,2703
93
+ qubx/loggers/__init__.py,sha256=nA7nLQKkR9hIJCYQyZxikm--xsB6DaTE5itKypEPBKA,443
94
+ qubx/loggers/csv.py,sha256=toihEqLtv_TuaMNmrQej9hmikijG4bM8X1du3QP6w4k,3790
95
+ qubx/loggers/factory.py,sha256=pDwLuFPPpoCCTiVoDrzvcAsyPFm6sS0e4Zi3j5UEhqk,1791
96
+ qubx/loggers/inmemory.py,sha256=5BcvEGLxwVD6u3c1eqsO2AVdWZawjJ8A-tJ-OCzzj5M,2965
97
+ qubx/loggers/mongo.py,sha256=yfCq29kIHdn1a6nDA6TKlc-yQpZ54mvbaZ0AmzY9Ydg,2658
98
98
  qubx/math/__init__.py,sha256=ltHSQj40sCBm3owcvtoZp34h6ws7pZCFcSZgUkTsUCY,114
99
99
  qubx/math/stats.py,sha256=uXm4NpBRxuHFTjXERv8rjM0MAJof8zr1Cklyra4CcBA,4056
100
100
  qubx/notifications/__init__.py,sha256=cb3DGxuiA8UwSTlTeF5pQKy4-vBef3gMeKtfcxEjdN4,547
101
101
  qubx/notifications/composite.py,sha256=fa-rvHEn6k-Fma5N7cT-7Sk7hzVyB0KDs2ktDyoyLxM,2689
102
- qubx/notifications/slack.py,sha256=FZ0zfTA-zRHOsOsIVBtM7mkt4m-adiuiV5yrwliS9RM,7976
102
+ qubx/notifications/slack.py,sha256=RWsLyL4lm6tbmrTlXQo3nPlfiLVJ0vCfY5toJ9G8RWU,8316
103
103
  qubx/notifications/throttler.py,sha256=8jnymPQbrgtN1rD7REQa2sA9teSWTqkk_uT9oaknOyc,5618
104
104
  qubx/pandaz/__init__.py,sha256=6BYz6gSgxjNa7WP1XqWflYG7WIq1ppSD9h1XGR5M5YQ,682
105
105
  qubx/pandaz/ta.py,sha256=sIX9YxxB2S2nWU4vnS4rXFuEI5WSY76Ky1TFwf9RhMw,92154
@@ -113,18 +113,18 @@ qubx/resources/instruments/symbols-bitfinex.json,sha256=CpzoVgWzGZRN6RpUNhtJVxa3
113
113
  qubx/resources/instruments/symbols-kraken.f.json,sha256=lwNqml3H7lNUl1h3siySSyE1MRcGfqfhb6BcxLsiKr0,212258
114
114
  qubx/resources/instruments/symbols-kraken.json,sha256=RjUTvkQuuu7V1HfSQREvnA4qqkdkB3-rzykDaQds2rQ,456544
115
115
  qubx/restarts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
116
- qubx/restarts/state_resolvers.py,sha256=GJ617qwpulqMp_-WhpmsLozQobxgt5lU4ZGOIUaUzas,5606
116
+ qubx/restarts/state_resolvers.py,sha256=xzQEFPGbYFYShSSb0cB3RvWl-8npRau_HrrMgTccSc0,5614
117
117
  qubx/restarts/time_finders.py,sha256=r7yyRhJByV2uqdgamDRX2XClwpWWI9BNpc80t9nk6c0,2448
118
118
  qubx/restorers/__init__.py,sha256=vrnZBPJHR0-6knAccj4bK0tkjUPNRl32qiLr5Mv4aR0,911
119
119
  qubx/restorers/balance.py,sha256=yLV1vBki0XhBxrOhgaJBHuuL8VmIii82LAWgLxusbcE,6967
120
120
  qubx/restorers/factory.py,sha256=eoijcUHDaBVPHSfkjyo1AHvWTvvs0kj7jJbF_NE30aw,6737
121
121
  qubx/restorers/interfaces.py,sha256=CcjBWavKq8_GIMKTSPodMa-n3wJQwcQTwyvYyNo_J3c,1776
122
122
  qubx/restorers/position.py,sha256=jMJjq2ZJwHpAlG45bMy49WvkYK5UylDiExt7nVpxCfg,8703
123
- qubx/restorers/signal.py,sha256=x-VLSnb863U517sPeZhGvfuIOUfmYVhANg5P7lio_Mg,11630
123
+ qubx/restorers/signal.py,sha256=0QFoy7OzDkK6AAmJEbbmSsHwmAhjMJYYggVFuLraKjk,10899
124
124
  qubx/restorers/state.py,sha256=dLaVnUwRCNRkUqbYyi0RfZs3Q3AdglkI_qTtQ8GDD5Y,7289
125
125
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
126
126
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
127
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=eci5pBLi6C5wdeMg8XaDZetvc5KOGai0J7B8X5uhuSs,654440
127
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=PwEMehN97zxTw_faYmjrDUHu_8-QlfsjOxo3h7bPQKo,654440
128
128
  qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
129
129
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
130
130
  qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
@@ -156,13 +156,13 @@ qubx/utils/questdb.py,sha256=TdjmlGPoZXdjidZ_evcBIkFtoL4nGQXPR4IQSUc6IvA,2509
156
156
  qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
157
157
  qubx/utils/runner/_jupyter_runner.pyt,sha256=fDj4AUs25jsdGmY9DDeSFufH1JkVhLFwy0BOmVO7nIU,9609
158
158
  qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
159
- qubx/utils/runner/configs.py,sha256=CtNXX0Ycnv4zeyqYVpr3i_2rK21jqYuzJcqM_m75x2U,3670
160
- qubx/utils/runner/factory.py,sha256=8IxKvsEL0opx5OlO4XEQgAlmu05sgxXxkY4G2MQdLbg,14941
161
- qubx/utils/runner/runner.py,sha256=iL929cwv75sSIBREWxTj4gG5mImn0qG2bX-9ck3ryY4,30152
159
+ qubx/utils/runner/configs.py,sha256=snVZJun6rBC09QZVaUd7BhqNlDZqmDMG7R8gHJeuSkU,3713
160
+ qubx/utils/runner/factory.py,sha256=eM4-Etcq-FewD2AjH_srFGzP413pm8er95KIZixXRpM,15152
161
+ qubx/utils/runner/runner.py,sha256=9s7mu84U29jCE7FtdW_yKKTzQfaXmCSvANF5cb7xd_Y,31399
162
162
  qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
163
163
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
164
- qubx-0.6.49.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
165
- qubx-0.6.49.dist-info/METADATA,sha256=ohYiVM7Tgtzai41K_bOb-vO3Ka-EakDzmSA40RlVfYY,4612
166
- qubx-0.6.49.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
167
- qubx-0.6.49.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
168
- qubx-0.6.49.dist-info/RECORD,,
164
+ qubx-0.6.52.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
165
+ qubx-0.6.52.dist-info/METADATA,sha256=Hor3x6zOv1rKp_mKkMdFc6HqE5bWIh14oHlX8ignRLk,4612
166
+ qubx-0.6.52.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
167
+ qubx-0.6.52.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
168
+ qubx-0.6.52.dist-info/RECORD,,
File without changes
File without changes