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 +22 -17
- qubx/backtester/simulator.py +103 -60
- qubx/backtester/utils.py +65 -23
- qubx/core/loggers.py +4 -6
- qubx/core/mixins/processing.py +53 -3
- qubx/core/mixins/universe.py +4 -3
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/data/composite.py +11 -2
- qubx/data/readers.py +5 -0
- qubx/emitters/base.py +1 -1
- qubx/loggers/__init__.py +1 -1
- qubx/loggers/csv.py +2 -2
- qubx/loggers/factory.py +5 -6
- qubx/loggers/inmemory.py +38 -32
- qubx/loggers/mongo.py +22 -27
- qubx/notifications/slack.py +14 -8
- qubx/restarts/state_resolvers.py +2 -2
- qubx/restorers/signal.py +2 -23
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/runner/configs.py +1 -0
- qubx/utils/runner/factory.py +12 -5
- qubx/utils/runner/runner.py +34 -6
- {qubx-0.6.49.dist-info → qubx-0.6.52.dist-info}/METADATA +1 -1
- {qubx-0.6.49.dist-info → qubx-0.6.52.dist-info}/RECORD +28 -28
- {qubx-0.6.49.dist-info → qubx-0.6.52.dist-info}/LICENSE +0 -0
- {qubx-0.6.49.dist-info → qubx-0.6.52.dist-info}/WHEEL +0 -0
- {qubx-0.6.49.dist-info → qubx-0.6.52.dist-info}/entry_points.txt +0 -0
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
qubx/backtester/simulator.py
CHANGED
|
@@ -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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
setup.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
qubx/core/mixins/universe.py
CHANGED
|
@@ -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.
|
|
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,
|
|
120
|
+
self._strategy.on_universe_change(self._context, to_add, [])
|
|
120
121
|
self._subscription_manager.commit()
|
|
121
|
-
self._instruments.update(
|
|
122
|
+
self._instruments.update(to_add)
|
|
122
123
|
|
|
123
124
|
def remove_instruments(
|
|
124
125
|
self,
|
|
Binary file
|
|
Binary file
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
|
-
|
|
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:
|
|
11
|
-
_execs:
|
|
12
|
-
_signals:
|
|
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:
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
36
|
-
)
|
|
37
|
-
self.db[f"{collection_name_prefix}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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:
|
|
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:
|
|
59
|
+
def write_data(self, log_type: str, data: list[dict[str, Any]]):
|
|
71
60
|
if len(data) > 0:
|
|
72
|
-
self.pool.apply_async(
|
|
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()
|
qubx/notifications/slack.py
CHANGED
|
@@ -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
|
-
#
|
|
79
|
-
if throttle_key is not None
|
|
80
|
-
|
|
81
|
-
|
|
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:
|
qubx/restarts/state_resolvers.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
214
|
+
{"$match": {"log_type": "signals", "strategy_name": self.strategy_name}},
|
|
236
215
|
{"$sort": {"timestamp": -1}},
|
|
237
216
|
{
|
|
238
217
|
"$group": {
|
|
Binary file
|
qubx/utils/runner/configs.py
CHANGED
qubx/utils/runner/factory.py
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
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
|
|
qubx/utils/runner/runner.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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)
|
|
@@ -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=
|
|
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=
|
|
14
|
-
qubx/backtester/utils.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
56
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
94
|
-
qubx/loggers/csv.py,sha256=
|
|
95
|
-
qubx/loggers/factory.py,sha256=
|
|
96
|
-
qubx/loggers/inmemory.py,sha256=
|
|
97
|
-
qubx/loggers/mongo.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
160
|
-
qubx/utils/runner/factory.py,sha256=
|
|
161
|
-
qubx/utils/runner/runner.py,sha256=
|
|
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.
|
|
165
|
-
qubx-0.6.
|
|
166
|
-
qubx-0.6.
|
|
167
|
-
qubx-0.6.
|
|
168
|
-
qubx-0.6.
|
|
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
|
|
File without changes
|