Qubx 0.6.64__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.65__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 +1 -1
- qubx/backtester/runner.py +4 -0
- qubx/backtester/simulated_data.py +11 -9
- qubx/backtester/simulator.py +26 -1
- qubx/backtester/utils.py +23 -10
- qubx/core/metrics.py +110 -5
- qubx/core/mixins/processing.py +8 -6
- 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/readers.py +18 -3
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/trackers/__init__.py +2 -0
- qubx/trackers/sizers.py +56 -0
- {qubx-0.6.64.dist-info → qubx-0.6.65.dist-info}/METADATA +1 -1
- {qubx-0.6.64.dist-info → qubx-0.6.65.dist-info}/RECORD +18 -18
- {qubx-0.6.64.dist-info → qubx-0.6.65.dist-info}/LICENSE +0 -0
- {qubx-0.6.64.dist-info → qubx-0.6.65.dist-info}/WHEEL +0 -0
- {qubx-0.6.64.dist-info → qubx-0.6.65.dist-info}/entry_points.txt +0 -0
qubx/backtester/data.py
CHANGED
|
@@ -71,7 +71,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
71
71
|
# Check if the instrument was actually subscribed (not filtered out)
|
|
72
72
|
if not self.has_subscription(i, subscription_type):
|
|
73
73
|
continue
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
h_data = self._data_source.peek_historical_data(i, subscription_type)
|
|
76
76
|
if h_data:
|
|
77
77
|
# _s_type = DataType.from_str(subscription_type)[0]
|
qubx/backtester/runner.py
CHANGED
|
@@ -422,6 +422,10 @@ class SimulationRunner:
|
|
|
422
422
|
logger.debug(f"[<y>simulator</y>] :: Setting default schedule: {self.data_config.default_trigger_schedule}")
|
|
423
423
|
ctx.set_event_schedule(self.data_config.default_trigger_schedule)
|
|
424
424
|
|
|
425
|
+
if self.setup.enable_funding:
|
|
426
|
+
logger.debug("[<y>simulator</y>] :: Enabling funding rate simulation")
|
|
427
|
+
ctx.subscribe(DataType.FUNDING_PAYMENT)
|
|
428
|
+
|
|
425
429
|
self.logs_writer = logs_writer
|
|
426
430
|
self.channel = channel
|
|
427
431
|
self.time_provider = simulated_clock
|
|
@@ -259,14 +259,14 @@ class IterableSimulationData(Iterator):
|
|
|
259
259
|
def _filter_instruments_for_subscription(self, data_type: str, instruments: list[Instrument]) -> list[Instrument]:
|
|
260
260
|
"""
|
|
261
261
|
Filter instruments based on subscription type requirements.
|
|
262
|
-
|
|
262
|
+
|
|
263
263
|
For funding payment subscriptions, only SWAP instruments are supported since
|
|
264
264
|
funding payments are specific to perpetual swap contracts.
|
|
265
|
-
|
|
265
|
+
|
|
266
266
|
Args:
|
|
267
267
|
data_type: The data type being subscribed to
|
|
268
268
|
instruments: List of instruments to filter
|
|
269
|
-
|
|
269
|
+
|
|
270
270
|
Returns:
|
|
271
271
|
Filtered list of instruments appropriate for the subscription type
|
|
272
272
|
"""
|
|
@@ -275,23 +275,25 @@ class IterableSimulationData(Iterator):
|
|
|
275
275
|
original_count = len(instruments)
|
|
276
276
|
filtered_instruments = [i for i in instruments if i.market_type == MarketType.SWAP]
|
|
277
277
|
filtered_count = len(filtered_instruments)
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
# Log if instruments were filtered out (debug info)
|
|
280
280
|
if filtered_count < original_count:
|
|
281
|
-
logger.debug(
|
|
282
|
-
|
|
281
|
+
logger.debug(
|
|
282
|
+
f"Filtered {original_count - filtered_count} non-SWAP instruments from funding payment subscription"
|
|
283
|
+
)
|
|
284
|
+
|
|
283
285
|
return filtered_instruments
|
|
284
|
-
|
|
286
|
+
|
|
285
287
|
# For all other subscription types, return instruments unchanged
|
|
286
288
|
return instruments
|
|
287
289
|
|
|
288
290
|
def add_instruments_for_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
|
|
289
291
|
instruments = instruments if isinstance(instruments, list) else [instruments]
|
|
290
292
|
_subt_key, _data_type, _params = self._parse_subscription_spec(subscription)
|
|
291
|
-
|
|
293
|
+
|
|
292
294
|
# Filter instruments based on subscription type requirements
|
|
293
295
|
instruments = self._filter_instruments_for_subscription(_data_type, instruments)
|
|
294
|
-
|
|
296
|
+
|
|
295
297
|
# If no instruments remain after filtering, skip subscription
|
|
296
298
|
if not instruments:
|
|
297
299
|
return
|
qubx/backtester/simulator.py
CHANGED
|
@@ -4,6 +4,7 @@ import pandas as pd
|
|
|
4
4
|
from joblib import delayed
|
|
5
5
|
|
|
6
6
|
from qubx import QubxLogConfig, logger
|
|
7
|
+
from qubx.backtester.utils import SetupTypes
|
|
7
8
|
from qubx.core.basics import Instrument
|
|
8
9
|
from qubx.core.exceptions import SimulationError
|
|
9
10
|
from qubx.core.metrics import TradingSessionResult
|
|
@@ -43,6 +44,7 @@ def simulate(
|
|
|
43
44
|
accurate_stop_orders_execution: bool = False,
|
|
44
45
|
signal_timeframe: str = "1Min",
|
|
45
46
|
open_close_time_indent_secs=1,
|
|
47
|
+
enable_funding: bool = False,
|
|
46
48
|
debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
|
|
47
49
|
show_latency_report: bool = False,
|
|
48
50
|
portfolio_log_freq: str = "5Min",
|
|
@@ -70,6 +72,7 @@ def simulate(
|
|
|
70
72
|
- accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
|
|
71
73
|
- signal_timeframe (str): Timeframe for signals, default is "1Min".
|
|
72
74
|
- open_close_time_indent_secs (int): Time indent in seconds for open/close times, default is 1.
|
|
75
|
+
- enable_funding (bool): If True, enables funding rate simulation, default is False.
|
|
73
76
|
- debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None): Logging level for debugging.
|
|
74
77
|
- show_latency_report: If True, shows simulator's latency report.
|
|
75
78
|
- portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
|
|
@@ -114,6 +117,7 @@ def simulate(
|
|
|
114
117
|
signal_timeframe=signal_timeframe,
|
|
115
118
|
accurate_stop_orders_execution=accurate_stop_orders_execution,
|
|
116
119
|
run_separate_instruments=run_separate_instruments,
|
|
120
|
+
enable_funding=enable_funding,
|
|
117
121
|
)
|
|
118
122
|
if not simulation_setups:
|
|
119
123
|
logger.error(
|
|
@@ -204,6 +208,7 @@ def _run_setups(
|
|
|
204
208
|
portfolio_log_freq,
|
|
205
209
|
enable_inmemory_emitter,
|
|
206
210
|
emitter_stats_interval,
|
|
211
|
+
close_data_readers=True,
|
|
207
212
|
)
|
|
208
213
|
for id, setup in enumerate(strategies_setups)
|
|
209
214
|
)
|
|
@@ -219,6 +224,20 @@ def _run_setups(
|
|
|
219
224
|
return successful_reports
|
|
220
225
|
|
|
221
226
|
|
|
227
|
+
def _adjust_start_date_for_min_instrument_onboard(setup: SimulationSetup, start: pd.Timestamp) -> pd.Timestamp:
|
|
228
|
+
"""
|
|
229
|
+
Adjust the start date for the simulation to the onboard date of the instrument with the minimum onboard date.
|
|
230
|
+
"""
|
|
231
|
+
onboard_dates = [
|
|
232
|
+
pd.Timestamp(instrument.onboard_date).replace(tzinfo=None)
|
|
233
|
+
for instrument in setup.instruments
|
|
234
|
+
if instrument.onboard_date is not None
|
|
235
|
+
]
|
|
236
|
+
if not onboard_dates:
|
|
237
|
+
return start
|
|
238
|
+
return max(start, min(onboard_dates))
|
|
239
|
+
|
|
240
|
+
|
|
222
241
|
def _run_setup(
|
|
223
242
|
setup_id: int,
|
|
224
243
|
account_id: str,
|
|
@@ -231,12 +250,18 @@ def _run_setup(
|
|
|
231
250
|
portfolio_log_freq: str,
|
|
232
251
|
enable_inmemory_emitter: bool = False,
|
|
233
252
|
emitter_stats_interval: str = "1h",
|
|
253
|
+
close_data_readers: bool = False,
|
|
234
254
|
) -> TradingSessionResult | None:
|
|
235
255
|
try:
|
|
236
256
|
emitter = None
|
|
237
257
|
emitter_data = None
|
|
238
258
|
if enable_inmemory_emitter:
|
|
239
259
|
emitter = InMemoryMetricEmitter(stats_interval=emitter_stats_interval)
|
|
260
|
+
|
|
261
|
+
# TODO: this can be removed once we add some artificial data stream to move the simulation
|
|
262
|
+
if setup.setup_type in [SetupTypes.SIGNAL, SetupTypes.SIGNAL_AND_TRACKER]:
|
|
263
|
+
start = _adjust_start_date_for_min_instrument_onboard(setup, start)
|
|
264
|
+
|
|
240
265
|
runner = SimulationRunner(
|
|
241
266
|
setup=setup,
|
|
242
267
|
data_config=data_setup,
|
|
@@ -252,7 +277,7 @@ def _run_setup(
|
|
|
252
277
|
level=QubxLogConfig.get_log_level(), custom_formatter=SimulatedLogFormatter(runner.ctx).formatter
|
|
253
278
|
)
|
|
254
279
|
|
|
255
|
-
runner.run(silent=silent)
|
|
280
|
+
runner.run(silent=silent, close_data_readers=close_data_readers)
|
|
256
281
|
|
|
257
282
|
# - service latency report
|
|
258
283
|
if show_latency_report:
|
qubx/backtester/utils.py
CHANGED
|
@@ -90,6 +90,7 @@ class SimulationSetup:
|
|
|
90
90
|
commissions: str | dict[str, str | None] | None = None
|
|
91
91
|
signal_timeframe: str = "1Min"
|
|
92
92
|
accurate_stop_orders_execution: bool = False
|
|
93
|
+
enable_funding: bool = False
|
|
93
94
|
|
|
94
95
|
def __str__(self) -> str:
|
|
95
96
|
return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchanges}[{self.commissions}]"
|
|
@@ -463,6 +464,7 @@ def recognize_simulation_configuration(
|
|
|
463
464
|
signal_timeframe: str,
|
|
464
465
|
accurate_stop_orders_execution: bool,
|
|
465
466
|
run_separate_instruments: bool = False,
|
|
467
|
+
enable_funding: bool = False,
|
|
466
468
|
) -> list[SimulationSetup]:
|
|
467
469
|
"""
|
|
468
470
|
Recognize and create setups based on the provided simulation configuration.
|
|
@@ -483,6 +485,7 @@ def recognize_simulation_configuration(
|
|
|
483
485
|
- signal_timeframe (str): Timeframe for generated signals.
|
|
484
486
|
- accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
|
|
485
487
|
- run_separate_instruments (bool): If True, creates separate setups for each instrument.
|
|
488
|
+
- enable_funding (bool): If True, enables funding rate simulation, default is False.
|
|
486
489
|
|
|
487
490
|
Returns:
|
|
488
491
|
- list[SimulationSetup]: A list of SimulationSetup objects, each representing a
|
|
@@ -503,7 +506,8 @@ def recognize_simulation_configuration(
|
|
|
503
506
|
r.extend(
|
|
504
507
|
recognize_simulation_configuration(
|
|
505
508
|
_n + n, v, instruments, exchanges, capital, basic_currency, commissions,
|
|
506
|
-
signal_timeframe, accurate_stop_orders_execution, run_separate_instruments
|
|
509
|
+
signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
|
|
510
|
+
enable_funding
|
|
507
511
|
)
|
|
508
512
|
)
|
|
509
513
|
|
|
@@ -524,12 +528,14 @@ def recognize_simulation_configuration(
|
|
|
524
528
|
if run_separate_instruments:
|
|
525
529
|
# Create separate setups for each instrument
|
|
526
530
|
for instrument in setup_instruments:
|
|
531
|
+
_s1 = c1[instrument.symbol] if isinstance(_s, pd.DataFrame) else _s
|
|
527
532
|
r.append(
|
|
528
533
|
SimulationSetup(
|
|
529
|
-
_t, f"{name}/{instrument.symbol}",
|
|
534
|
+
_t, f"{name}/{instrument.symbol}", _s1, c1, # type: ignore
|
|
530
535
|
[instrument],
|
|
531
536
|
exchanges, capital, basic_currency, commissions,
|
|
532
|
-
signal_timeframe, accurate_stop_orders_execution
|
|
537
|
+
signal_timeframe, accurate_stop_orders_execution,
|
|
538
|
+
enable_funding
|
|
533
539
|
)
|
|
534
540
|
)
|
|
535
541
|
else:
|
|
@@ -538,7 +544,8 @@ def recognize_simulation_configuration(
|
|
|
538
544
|
_t, name, _s, c1, # type: ignore
|
|
539
545
|
setup_instruments,
|
|
540
546
|
exchanges, capital, basic_currency, commissions,
|
|
541
|
-
signal_timeframe, accurate_stop_orders_execution
|
|
547
|
+
signal_timeframe, accurate_stop_orders_execution,
|
|
548
|
+
enable_funding
|
|
542
549
|
)
|
|
543
550
|
)
|
|
544
551
|
else:
|
|
@@ -547,7 +554,8 @@ def recognize_simulation_configuration(
|
|
|
547
554
|
recognize_simulation_configuration(
|
|
548
555
|
# name + "/" + str(j), s, instruments, exchange, capital, basic_currency, commissions
|
|
549
556
|
name, s, instruments, exchanges, capital, basic_currency, commissions, # type: ignore
|
|
550
|
-
signal_timeframe, accurate_stop_orders_execution, run_separate_instruments
|
|
557
|
+
signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
|
|
558
|
+
enable_funding
|
|
551
559
|
)
|
|
552
560
|
)
|
|
553
561
|
|
|
@@ -560,7 +568,8 @@ def recognize_simulation_configuration(
|
|
|
560
568
|
SetupTypes.STRATEGY,
|
|
561
569
|
f"{name}/{instrument.symbol}", configs, None, [instrument],
|
|
562
570
|
exchanges, capital, basic_currency, commissions,
|
|
563
|
-
signal_timeframe, accurate_stop_orders_execution
|
|
571
|
+
signal_timeframe, accurate_stop_orders_execution,
|
|
572
|
+
enable_funding
|
|
564
573
|
)
|
|
565
574
|
)
|
|
566
575
|
else:
|
|
@@ -569,7 +578,8 @@ def recognize_simulation_configuration(
|
|
|
569
578
|
SetupTypes.STRATEGY,
|
|
570
579
|
name, configs, None, instruments,
|
|
571
580
|
exchanges, capital, basic_currency, commissions,
|
|
572
|
-
signal_timeframe, accurate_stop_orders_execution
|
|
581
|
+
signal_timeframe, accurate_stop_orders_execution,
|
|
582
|
+
enable_funding
|
|
573
583
|
)
|
|
574
584
|
)
|
|
575
585
|
|
|
@@ -581,12 +591,14 @@ def recognize_simulation_configuration(
|
|
|
581
591
|
if run_separate_instruments:
|
|
582
592
|
# Create separate setups for each instrument
|
|
583
593
|
for instrument in setup_instruments:
|
|
594
|
+
_c1 = c1[instrument.symbol] if isinstance(c1, pd.DataFrame) else c1
|
|
584
595
|
r.append(
|
|
585
596
|
SimulationSetup(
|
|
586
597
|
SetupTypes.SIGNAL,
|
|
587
|
-
f"{name}/{instrument.symbol}",
|
|
598
|
+
f"{name}/{instrument.symbol}", _c1, None, [instrument],
|
|
588
599
|
exchanges, capital, basic_currency, commissions,
|
|
589
|
-
signal_timeframe, accurate_stop_orders_execution
|
|
600
|
+
signal_timeframe, accurate_stop_orders_execution,
|
|
601
|
+
enable_funding
|
|
590
602
|
)
|
|
591
603
|
)
|
|
592
604
|
else:
|
|
@@ -595,7 +607,8 @@ def recognize_simulation_configuration(
|
|
|
595
607
|
SetupTypes.SIGNAL,
|
|
596
608
|
name, c1, None, setup_instruments,
|
|
597
609
|
exchanges, capital, basic_currency, commissions,
|
|
598
|
-
signal_timeframe, accurate_stop_orders_execution
|
|
610
|
+
signal_timeframe, accurate_stop_orders_execution,
|
|
611
|
+
enable_funding
|
|
599
612
|
)
|
|
600
613
|
)
|
|
601
614
|
|
qubx/core/metrics.py
CHANGED
|
@@ -1131,11 +1131,28 @@ def find_session(sessions: list[TradingSessionResult], name: str) -> TradingSess
|
|
|
1131
1131
|
raise ValueError(f"Session with name {name} not found")
|
|
1132
1132
|
|
|
1133
1133
|
|
|
1134
|
-
def find_sessions(sessions: list[TradingSessionResult],
|
|
1134
|
+
def find_sessions(sessions: list[TradingSessionResult], *names: str) -> list[TradingSessionResult]:
|
|
1135
1135
|
"""
|
|
1136
|
-
Match
|
|
1136
|
+
Match sessions by regex patterns or substrings. Returns sessions that match at least one of the provided names.
|
|
1137
|
+
|
|
1138
|
+
Args:
|
|
1139
|
+
sessions: List of TradingSessionResult objects to search through
|
|
1140
|
+
*names: One or more name patterns to match against
|
|
1141
|
+
|
|
1142
|
+
Returns:
|
|
1143
|
+
List of sessions where the name matches at least one of the provided patterns
|
|
1137
1144
|
"""
|
|
1138
|
-
|
|
1145
|
+
if not names:
|
|
1146
|
+
return []
|
|
1147
|
+
|
|
1148
|
+
matched_sessions = []
|
|
1149
|
+
for s in sessions:
|
|
1150
|
+
for name in names:
|
|
1151
|
+
if re.match(name, s.name) or name in s.name:
|
|
1152
|
+
matched_sessions.append(s)
|
|
1153
|
+
break # Don't add the same session multiple times
|
|
1154
|
+
|
|
1155
|
+
return matched_sessions
|
|
1139
1156
|
|
|
1140
1157
|
|
|
1141
1158
|
def tearsheet(
|
|
@@ -1403,6 +1420,90 @@ def calculate_leverage(
|
|
|
1403
1420
|
return (value.squeeze() / capital).mul(100).rename("Leverage") # type: ignore
|
|
1404
1421
|
|
|
1405
1422
|
|
|
1423
|
+
def calculate_leverage_per_symbol(
|
|
1424
|
+
session: TradingSessionResult, start: str | pd.Timestamp | None = None
|
|
1425
|
+
) -> pd.DataFrame:
|
|
1426
|
+
"""
|
|
1427
|
+
Calculate leverage for each symbol in the trading session.
|
|
1428
|
+
|
|
1429
|
+
Args:
|
|
1430
|
+
session: TradingSessionResult containing portfolio data and capital info
|
|
1431
|
+
start: Optional start timestamp for calculation (defaults to session start)
|
|
1432
|
+
|
|
1433
|
+
Returns:
|
|
1434
|
+
pd.DataFrame with columns for each symbol showing their leverage percentage over time
|
|
1435
|
+
"""
|
|
1436
|
+
portfolio = session.portfolio_log
|
|
1437
|
+
init_capital = session.get_total_capital()
|
|
1438
|
+
start = start or session.start
|
|
1439
|
+
|
|
1440
|
+
# Calculate total capital (same for all symbols)
|
|
1441
|
+
total_pnl = calculate_total_pnl(portfolio, split_cumulative=False).loc[start:]
|
|
1442
|
+
capital = init_capital + total_pnl["Total_PnL"].cumsum() - total_pnl["Total_Commissions"].cumsum()
|
|
1443
|
+
|
|
1444
|
+
# Extract unique symbols from column names
|
|
1445
|
+
value_columns = [col for col in portfolio.columns if "_Value" in col]
|
|
1446
|
+
symbols = sorted(list(set(col.split("_")[0] for col in value_columns)))
|
|
1447
|
+
|
|
1448
|
+
# Calculate leverage for each symbol
|
|
1449
|
+
leverages = {}
|
|
1450
|
+
for symbol in symbols:
|
|
1451
|
+
value = portfolio.filter(regex=f"{symbol}_Value").loc[start:].sum(axis=1)
|
|
1452
|
+
if not value.empty:
|
|
1453
|
+
leverages[symbol] = (value.squeeze() / capital).mul(100)
|
|
1454
|
+
|
|
1455
|
+
return pd.DataFrame(leverages)
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
def calculate_pnl_per_symbol(
|
|
1459
|
+
session: TradingSessionResult,
|
|
1460
|
+
include_commissions: bool = True,
|
|
1461
|
+
pct_from_initial_capital: bool = True,
|
|
1462
|
+
start: str | pd.Timestamp | None = None,
|
|
1463
|
+
) -> pd.DataFrame:
|
|
1464
|
+
"""
|
|
1465
|
+
Calculate PnL for each symbol in the trading session.
|
|
1466
|
+
|
|
1467
|
+
Args:
|
|
1468
|
+
session: TradingSessionResult containing portfolio data
|
|
1469
|
+
cumulative: If True, return cumulative PnL; if False, return per-period PnL
|
|
1470
|
+
include_commissions: If True, subtract commissions from PnL
|
|
1471
|
+
start: Optional start timestamp for calculation (defaults to session start)
|
|
1472
|
+
|
|
1473
|
+
Returns:
|
|
1474
|
+
pd.DataFrame with columns for each symbol showing their PnL over time
|
|
1475
|
+
"""
|
|
1476
|
+
portfolio = session.portfolio_log
|
|
1477
|
+
start = start or session.start
|
|
1478
|
+
init_capital = session.get_total_capital()
|
|
1479
|
+
|
|
1480
|
+
# Extract unique symbols from PnL columns
|
|
1481
|
+
pnl_columns = [col for col in portfolio.columns if "_PnL" in col]
|
|
1482
|
+
symbols = sorted(list(set(col.split("_")[0] for col in pnl_columns)))
|
|
1483
|
+
|
|
1484
|
+
# Calculate PnL for each symbol
|
|
1485
|
+
pnls = {}
|
|
1486
|
+
for symbol in symbols:
|
|
1487
|
+
# Get PnL for this symbol
|
|
1488
|
+
symbol_pnl = portfolio.filter(regex=f"{symbol}_PnL").loc[start:]
|
|
1489
|
+
|
|
1490
|
+
if not symbol_pnl.empty:
|
|
1491
|
+
pnl_series = symbol_pnl.squeeze()
|
|
1492
|
+
|
|
1493
|
+
if include_commissions:
|
|
1494
|
+
# Subtract commissions if requested
|
|
1495
|
+
symbol_comms = portfolio.filter(regex=f"{symbol}_Commissions").loc[start:]
|
|
1496
|
+
if not symbol_comms.empty:
|
|
1497
|
+
comm_series = symbol_comms.squeeze()
|
|
1498
|
+
pnl_series = pnl_series - comm_series
|
|
1499
|
+
|
|
1500
|
+
pnls[symbol] = pnl_series.cumsum()
|
|
1501
|
+
if pct_from_initial_capital:
|
|
1502
|
+
pnls[symbol] = round(pnls[symbol] / init_capital * 100, 2)
|
|
1503
|
+
|
|
1504
|
+
return pd.DataFrame(pnls)
|
|
1505
|
+
|
|
1506
|
+
|
|
1406
1507
|
def chart_signals(
|
|
1407
1508
|
result: TradingSessionResult,
|
|
1408
1509
|
symbol: str,
|
|
@@ -1546,14 +1647,18 @@ def get_symbol_pnls(
|
|
|
1546
1647
|
return pd.DataFrame(pnls, index=[s.name for s in session])
|
|
1547
1648
|
|
|
1548
1649
|
|
|
1549
|
-
def combine_sessions(
|
|
1650
|
+
def combine_sessions(
|
|
1651
|
+
sessions: list[TradingSessionResult], name: str = "Portfolio", scale_capital: bool = True
|
|
1652
|
+
) -> TradingSessionResult:
|
|
1550
1653
|
"""
|
|
1551
1654
|
DEPRECATED: use extend_trading_results instead
|
|
1552
1655
|
"""
|
|
1553
1656
|
session = copy(sessions[0])
|
|
1554
1657
|
session.name = name
|
|
1555
1658
|
session.instruments = list(set(chain.from_iterable([e.instruments for e in sessions])))
|
|
1556
|
-
session.capital = sessions[0].get_total_capital()
|
|
1659
|
+
session.capital = sessions[0].get_total_capital()
|
|
1660
|
+
if scale_capital:
|
|
1661
|
+
session.capital *= len(sessions)
|
|
1557
1662
|
session.portfolio_log = pd.concat(
|
|
1558
1663
|
[e.portfolio_log.loc[:, (e.portfolio_log != 0).any(axis=0)] for e in sessions], axis=1
|
|
1559
1664
|
)
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -540,10 +540,10 @@ class ProcessingManager(IProcessingManager):
|
|
|
540
540
|
current_time - self._last_data_ready_log_time
|
|
541
541
|
) >= td_64(10, "s")
|
|
542
542
|
if should_log:
|
|
543
|
-
logger.warning(
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
)
|
|
543
|
+
# logger.warning(
|
|
544
|
+
# f"No instruments ready after timeout - still waiting "
|
|
545
|
+
# f"({ready_instruments}/{total_instruments} ready)"
|
|
546
|
+
# )
|
|
547
547
|
self._last_data_ready_log_time = current_time
|
|
548
548
|
return False
|
|
549
549
|
|
|
@@ -734,10 +734,12 @@ class ProcessingManager(IProcessingManager):
|
|
|
734
734
|
base_update = self.__update_base_data(instrument, event_type, quote)
|
|
735
735
|
return MarketEvent(self._time_provider.time(), event_type, instrument, quote, is_trigger=base_update)
|
|
736
736
|
|
|
737
|
-
def _handle_funding_payment(
|
|
737
|
+
def _handle_funding_payment(
|
|
738
|
+
self, instrument: Instrument, event_type: str, funding_payment: FundingPayment
|
|
739
|
+
) -> MarketEvent:
|
|
738
740
|
# Apply funding payment to position
|
|
739
741
|
self._account.process_funding_payment(instrument, funding_payment)
|
|
740
|
-
|
|
742
|
+
|
|
741
743
|
# Continue with existing event processing
|
|
742
744
|
base_update = self.__update_base_data(instrument, event_type, funding_payment)
|
|
743
745
|
return MarketEvent(self._time_provider.time(), event_type, instrument, funding_payment, is_trigger=base_update)
|
|
Binary file
|
|
Binary file
|
qubx/data/readers.py
CHANGED
|
@@ -1107,9 +1107,13 @@ def _calculate_max_candles_chunksize(timeframe: str | None) -> int:
|
|
|
1107
1107
|
# Convert timeframe to pandas Timedelta
|
|
1108
1108
|
tf_delta = pd.Timedelta(timeframe)
|
|
1109
1109
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1110
|
+
if timeframe == "1d":
|
|
1111
|
+
one_month = pd.Timedelta("30d")
|
|
1112
|
+
max_candles = int(one_month / tf_delta)
|
|
1113
|
+
else:
|
|
1114
|
+
# Calculate how many candles fit in 1 week
|
|
1115
|
+
one_week = pd.Timedelta("7d")
|
|
1116
|
+
max_candles = int(one_week / tf_delta)
|
|
1113
1117
|
|
|
1114
1118
|
# Ensure we don't return 0 or negative values
|
|
1115
1119
|
return max(1, max_candles)
|
|
@@ -1152,6 +1156,11 @@ def _calculate_time_windows_for_chunking(
|
|
|
1152
1156
|
windows.append((current_start, current_end))
|
|
1153
1157
|
current_start = current_end
|
|
1154
1158
|
|
|
1159
|
+
# If last window is less than half of the window before it, then merge together
|
|
1160
|
+
if len(windows) > 1 and windows[-1][0] - windows[-1][1] < chunk_duration / 2:
|
|
1161
|
+
windows[-2] = (windows[-2][0], windows[-1][1])
|
|
1162
|
+
windows.pop()
|
|
1163
|
+
|
|
1155
1164
|
return windows
|
|
1156
1165
|
except (ValueError, TypeError):
|
|
1157
1166
|
# If timeframe can't be parsed, fall back to single window
|
|
@@ -1353,6 +1362,12 @@ class QuestDBConnector(DataReader):
|
|
|
1353
1362
|
self._connection = pg.connect(self.connection_url, autocommit=True)
|
|
1354
1363
|
logger.debug(f"Connected to QuestDB at {self._host}:{self._port}")
|
|
1355
1364
|
|
|
1365
|
+
def close(self):
|
|
1366
|
+
if self._connection:
|
|
1367
|
+
self._connection.close()
|
|
1368
|
+
self._connection = None
|
|
1369
|
+
logger.debug(f"Disconnected from QuestDB at {self._host}:{self._port}")
|
|
1370
|
+
|
|
1356
1371
|
def read(
|
|
1357
1372
|
self,
|
|
1358
1373
|
data_id: str,
|
|
Binary file
|
qubx/trackers/__init__.py
CHANGED
|
@@ -5,6 +5,7 @@ __all__ = [
|
|
|
5
5
|
"FixedRiskSizer",
|
|
6
6
|
"FixedLeverageSizer",
|
|
7
7
|
"LongShortRatioPortfolioSizer",
|
|
8
|
+
"InverseVolatilitySizer",
|
|
8
9
|
"FixedRiskSizerWithConstantCapital",
|
|
9
10
|
"ImprovedEntryTracker",
|
|
10
11
|
"ImprovedEntryTrackerDynamicTake",
|
|
@@ -26,5 +27,6 @@ from .sizers import (
|
|
|
26
27
|
FixedRiskSizer,
|
|
27
28
|
FixedRiskSizerWithConstantCapital,
|
|
28
29
|
FixedSizer,
|
|
30
|
+
InverseVolatilitySizer,
|
|
29
31
|
LongShortRatioPortfolioSizer,
|
|
30
32
|
)
|
qubx/trackers/sizers.py
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
2
3
|
|
|
3
4
|
from qubx import logger
|
|
4
5
|
from qubx.core.basics import Signal, TargetPosition
|
|
5
6
|
from qubx.core.interfaces import IPositionSizer, IStrategyContext
|
|
7
|
+
from qubx.ta.indicators import atr
|
|
8
|
+
from qubx.utils.time import infer_series_frequency
|
|
9
|
+
|
|
10
|
+
_S_YEAR = 24 * 3600 * 365
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def annual_factor(tframe_or_series: str | pd.Series) -> float:
|
|
14
|
+
timeframe = (
|
|
15
|
+
infer_series_frequency(tframe_or_series[:25]) if isinstance(tframe_or_series, pd.Series) else tframe_or_series
|
|
16
|
+
)
|
|
17
|
+
return _S_YEAR / pd.Timedelta(timeframe).total_seconds()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def annual_factor_sqrt(tframe_or_series: str | pd.Series) -> float:
|
|
21
|
+
return np.sqrt(annual_factor(tframe_or_series))
|
|
6
22
|
|
|
7
23
|
|
|
8
24
|
class FixedSizer(IPositionSizer):
|
|
@@ -231,3 +247,43 @@ class FixedRiskSizerWithConstantCapital(IPositionSizer):
|
|
|
231
247
|
t_pos.append(signal.target_for_amount(target_position_size))
|
|
232
248
|
|
|
233
249
|
return t_pos
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class InverseVolatilitySizer(IPositionSizer):
|
|
253
|
+
def __init__(
|
|
254
|
+
self,
|
|
255
|
+
target_risk: float,
|
|
256
|
+
atr_timeframe: str = "4h",
|
|
257
|
+
atr_period: int = 40,
|
|
258
|
+
atr_smoother: str = "sma",
|
|
259
|
+
divide_by_universe_size: bool = False,
|
|
260
|
+
) -> None:
|
|
261
|
+
self.target_risk = target_risk
|
|
262
|
+
self.atr_timeframe = atr_timeframe
|
|
263
|
+
self.atr_period = atr_period
|
|
264
|
+
self.atr_smoother = atr_smoother
|
|
265
|
+
self.divide_by_universe_size = divide_by_universe_size
|
|
266
|
+
|
|
267
|
+
def calculate_target_positions(self, ctx: IStrategyContext, signals: list[Signal]) -> list[TargetPosition]:
|
|
268
|
+
return [self._get_target_position(ctx, signal) for signal in signals]
|
|
269
|
+
|
|
270
|
+
def _get_target_position(self, ctx: IStrategyContext, signal: Signal) -> TargetPosition:
|
|
271
|
+
_ohlc = ctx.ohlc(signal.instrument, self.atr_timeframe, length=self.atr_period * 2)
|
|
272
|
+
_atr = atr(_ohlc, self.atr_period, self.atr_smoother, percentage=True)
|
|
273
|
+
if len(_atr) == 0 or np.isnan(_atr[1]):
|
|
274
|
+
return signal.target_for_amount(0)
|
|
275
|
+
|
|
276
|
+
_ann_vol_fraction = (_atr[1] * annual_factor_sqrt(self.atr_timeframe)) / 100.0
|
|
277
|
+
if np.isclose(_ann_vol_fraction, 0):
|
|
278
|
+
return signal.target_for_amount(0)
|
|
279
|
+
|
|
280
|
+
universe_size = len(ctx.instruments)
|
|
281
|
+
price = _ohlc[0].close
|
|
282
|
+
size = (
|
|
283
|
+
ctx.get_total_capital()
|
|
284
|
+
* (self.target_risk / _ann_vol_fraction)
|
|
285
|
+
* signal.signal
|
|
286
|
+
/ (universe_size if self.divide_by_universe_size else 1)
|
|
287
|
+
/ price
|
|
288
|
+
)
|
|
289
|
+
return signal.target_for_amount(size)
|
|
@@ -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=ZRvgMPXyM0t6tYQ18pPMfBtxbzrNPfvMgj0W7AY3txo,6867
|
|
7
7
|
qubx/backtester/management.py,sha256=FQSMkdrTZrxKdLRrf4Uiw60pdBMb0xESeFrTfH9AqZk,20713
|
|
8
8
|
qubx/backtester/ome.py,sha256=LnnSANMD2XBo18JtLRh96Ey9BH_StrfshQnCu2_aOc4,18646
|
|
9
9
|
qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
|
|
10
|
-
qubx/backtester/runner.py,sha256=
|
|
11
|
-
qubx/backtester/simulated_data.py,sha256=
|
|
10
|
+
qubx/backtester/runner.py,sha256=_QWaH-11-SaLAqRj3VYT3xKIkohDPqaz2niveMejUDI,20678
|
|
11
|
+
qubx/backtester/simulated_data.py,sha256=wgesN8mKMiIBDbLtFZUh6YDf1sHGC8-ilLCPIlfRXLg,19094
|
|
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=HAhkiUIztl7Q0zGtp1seThJDa85yBBm2gNlDjmlDgjw,13036
|
|
14
|
+
qubx/backtester/utils.py,sha256=f0SrcNI2ZBEwxQ04HqxLeoOJ4ycb29g_uiITnoztt2s,37192
|
|
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
|
|
@@ -46,25 +46,25 @@ qubx/core/initializer.py,sha256=YgTBs5LpIk6ZFdmMD8zCnJnVNcMh1oeYvt157jhwyMg,4242
|
|
|
46
46
|
qubx/core/interfaces.py,sha256=vsz9Bo-UapQVsPQ7jz6Oec4OHoUtgx0fEeAMqY9-GKg,62464
|
|
47
47
|
qubx/core/loggers.py,sha256=pa28UYLTfRibhDzcbtPfbtNb3jpMZ8catTMikA0RFlc,14268
|
|
48
48
|
qubx/core/lookups.py,sha256=2-yjMkijRY2jRTnT8hH4n5Lq2m9z9oBtyBPF8vwxKlc,18313
|
|
49
|
-
qubx/core/metrics.py,sha256=
|
|
49
|
+
qubx/core/metrics.py,sha256=DlDynG_YnfnY0s7AKhyWLMcgUPJZpN4SUUFadjbc9Tk,66054
|
|
50
50
|
qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
|
|
51
51
|
qubx/core/mixins/market.py,sha256=my4RvQIfvecIiqM-UzHJteEYakWl9JwhmgQ8OaGstN4,5173
|
|
52
|
-
qubx/core/mixins/processing.py,sha256
|
|
52
|
+
qubx/core/mixins/processing.py,sha256=-VL3m5rZcH96MfyXs_ZntoWayPoDDL2Lz5GSE2RX-lA,37831
|
|
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
55
|
qubx/core/mixins/universe.py,sha256=mzZJA7Me6HNFbAMGg1XOpnYCMtcFKHESTiozjaXyKXY,10100
|
|
56
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
56
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=EPZz0HPC7YLX8dPkrJVjB0h3cnxepXWHxk_y05Yh0NY,994760
|
|
57
57
|
qubx/core/series.pxd,sha256=aI5PG1hbr827xwcnSYgGMF2IBD4GvCRby_i9lrGrJdQ,4026
|
|
58
58
|
qubx/core/series.pyi,sha256=AHROxA9wer3W2ehgI08wPpDVc4gPkGbzvmkkA4K60Ts,4821
|
|
59
59
|
qubx/core/series.pyx,sha256=w7XtvAHPnov0Twov9c-xdhgQARVbSrTiIVi1Axo2VWQ,47925
|
|
60
|
-
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
60
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=tNu7GL0pTNl1dz86aNMkVYAh77wLwCNSQ6IBnSmvxI8,86568
|
|
61
61
|
qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
|
|
62
62
|
qubx/core/utils.pyx,sha256=UR9achMR-LARsztd2eelFsDsFH3n0gXACIKoGNPI9X4,1766
|
|
63
63
|
qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
|
|
64
64
|
qubx/data/composite.py,sha256=nLA3w3kMvzOc8n2rjAZ35sKa5DXAe8SOFsEov8PpES4,18881
|
|
65
65
|
qubx/data/helpers.py,sha256=GtU9fUJoqJiKiOJA6I6j2I7ikauT2jtkfMNryFsEMpE,17859
|
|
66
66
|
qubx/data/hft.py,sha256=be7AwzTOjqqCENn0ClrZoHDyKv3SFG66IyTp8QadHlM,33687
|
|
67
|
-
qubx/data/readers.py,sha256=
|
|
67
|
+
qubx/data/readers.py,sha256=FCrhQDfclQBZgoKMDY83z7j2Ga7dw44cf1D37VAxxpc,74420
|
|
68
68
|
qubx/data/registry.py,sha256=SqCZ9Q0BZiHW2gC9yRuiVRV0lejyJAHI-694Yl_Cfdo,3892
|
|
69
69
|
qubx/data/tardis.py,sha256=O-zglpusmO6vCY3arSOgH6KUbkfPajSAIQfMKlVmh_E,33878
|
|
70
70
|
qubx/emitters/__init__.py,sha256=MPs7ZRZZnURljusiuvlO5g8M4H1UjEfg5fkyKeJmIBI,791
|
|
@@ -131,16 +131,16 @@ qubx/restorers/signal.py,sha256=7n7eeRhWGUBPbg179GxFH_ifywcl3pQJbwrcDklw0N0,1460
|
|
|
131
131
|
qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
|
|
132
132
|
qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
|
|
133
133
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
134
|
-
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
134
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=VuZOYiQho7aPAASSulbHLYMtnOEltI1AbwaCNDgr-fg,662632
|
|
135
135
|
qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
|
|
136
136
|
qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
|
|
137
137
|
qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
|
|
138
|
-
qubx/trackers/__init__.py,sha256=
|
|
138
|
+
qubx/trackers/__init__.py,sha256=zvIahF8MwSffBMOX2BDFFNKJCWtL8TyFLQLL_DR22JM,1060
|
|
139
139
|
qubx/trackers/advanced.py,sha256=CONogr5hHHEwfolkegjpEz7NNk4Ruf9pOq5nKifNXVM,12761
|
|
140
140
|
qubx/trackers/composite.py,sha256=Tjupx78SraXmRKkWhu8n81RkPjOgsDbXLd8yz6PhbaA,6318
|
|
141
141
|
qubx/trackers/rebalancers.py,sha256=KFY7xuD4fGALiSLMas8MZ3ueRzlWt5wDT9329dlmNng,5150
|
|
142
142
|
qubx/trackers/riskctrl.py,sha256=O6UTk4nK7u5YaT_Sd4aSFBR3dWaxOLLzOzMoeU71hDY,35022
|
|
143
|
-
qubx/trackers/sizers.py,sha256=
|
|
143
|
+
qubx/trackers/sizers.py,sha256=u9HBcjyEdtYquDUXUaxW5bSI7mHWfJpM09WmvmM0w0k,11631
|
|
144
144
|
qubx/utils/__init__.py,sha256=FEPBtU3dhfLawBkAfm9FEUW4RuOY7pGCBfzDCtKjn9A,481
|
|
145
145
|
qubx/utils/_pyxreloader.py,sha256=34kNd8kQi2ey_ZrGdVVUHbPrO1PEiHZDLEDBscIkT_s,12292
|
|
146
146
|
qubx/utils/charting/lookinglass.py,sha256=m7lWU8c0E8tXzGbkN0GB8CL-kd92MnH_wD8cATX067k,39232
|
|
@@ -168,8 +168,8 @@ qubx/utils/runner/factory.py,sha256=eM4-Etcq-FewD2AjH_srFGzP413pm8er95KIZixXRpM,
|
|
|
168
168
|
qubx/utils/runner/runner.py,sha256=m58a3kEwSs1xfgg_s9FwrQJ3AZV4Lf_VOmKDPQdaWH8,31518
|
|
169
169
|
qubx/utils/time.py,sha256=qZVLcvp0AkG0JOuDiIgUk4znO8PUHWNEpMwr3z-ko7E,10170
|
|
170
170
|
qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
|
|
171
|
-
qubx-0.6.
|
|
172
|
-
qubx-0.6.
|
|
173
|
-
qubx-0.6.
|
|
174
|
-
qubx-0.6.
|
|
175
|
-
qubx-0.6.
|
|
171
|
+
qubx-0.6.65.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
|
|
172
|
+
qubx-0.6.65.dist-info/METADATA,sha256=o41M90rwzMsTL2X50Zg5vXtKn8erii6vL1H1pOI9Xb4,4612
|
|
173
|
+
qubx-0.6.65.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
|
|
174
|
+
qubx-0.6.65.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
175
|
+
qubx-0.6.65.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|