Qubx 0.6.40__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.41__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/_nb_magic.py +1 -0
- qubx/backtester/simulated_exchange.py +1 -3
- qubx/core/helpers.py +1 -16
- qubx/core/metrics.py +43 -1
- qubx/core/mixins/processing.py +15 -4
- 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/tardis.py +18 -14
- qubx/notifications/__init__.py +9 -1
- qubx/notifications/slack.py +43 -10
- qubx/notifications/throttler.py +182 -0
- qubx/pandaz/utils.py +5 -2
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/runner/factory.py +52 -1
- {qubx-0.6.40.dist-info → qubx-0.6.41.dist-info}/METADATA +1 -1
- {qubx-0.6.40.dist-info → qubx-0.6.41.dist-info}/RECORD +19 -18
- {qubx-0.6.40.dist-info → qubx-0.6.41.dist-info}/WHEEL +1 -1
- {qubx-0.6.40.dist-info → qubx-0.6.41.dist-info}/LICENSE +0 -0
- {qubx-0.6.40.dist-info → qubx-0.6.41.dist-info}/entry_points.txt +0 -0
qubx/_nb_magic.py
CHANGED
|
@@ -155,9 +155,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
|
|
|
155
155
|
if order.id == order_id:
|
|
156
156
|
return self._process_ome_response(o.cancel_order(order_id))
|
|
157
157
|
|
|
158
|
-
logger.
|
|
159
|
-
f"[<y>{self.__class__.__name__}</y>] :: cancel_order :: can't find order with id = 'ValueError{order_id}'!"
|
|
160
|
-
)
|
|
158
|
+
logger.warning(f"[<y>{self.__class__.__name__}</y>] :: cancel_order :: can't find order '{order_id}'!")
|
|
161
159
|
return None
|
|
162
160
|
|
|
163
161
|
ome = self._ome.get(instrument)
|
qubx/core/helpers.py
CHANGED
|
@@ -20,7 +20,7 @@ from qubx.utils.time import convert_seconds_to_str, convert_tf_str_td64, interva
|
|
|
20
20
|
|
|
21
21
|
class CachedMarketDataHolder:
|
|
22
22
|
"""
|
|
23
|
-
Collected cached data updates from
|
|
23
|
+
Collected cached data updates from market
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
default_timeframe: np.timedelta64
|
|
@@ -35,7 +35,6 @@ class CachedMarketDataHolder:
|
|
|
35
35
|
self._last_bar = defaultdict(lambda: None)
|
|
36
36
|
self._updates = dict()
|
|
37
37
|
self._instr_to_sub_to_buffer = defaultdict(lambda: defaultdict(lambda: deque(maxlen=max_buffer_size)))
|
|
38
|
-
self._ready_instruments = set()
|
|
39
38
|
if default_timeframe:
|
|
40
39
|
self.update_default_timeframe(default_timeframe)
|
|
41
40
|
|
|
@@ -68,19 +67,8 @@ class CachedMarketDataHolder:
|
|
|
68
67
|
self._ohlcvs = other._ohlcvs
|
|
69
68
|
self._updates = other._updates
|
|
70
69
|
self._instr_to_sub_to_buffer = other._instr_to_sub_to_buffer
|
|
71
|
-
self._ready_instruments = set() # reset the ready instruments
|
|
72
70
|
self._last_bar = defaultdict(lambda: None) # reset the last bar
|
|
73
71
|
|
|
74
|
-
def is_data_ready(self) -> bool:
|
|
75
|
-
"""
|
|
76
|
-
Check if at least one update was received for all instruments.
|
|
77
|
-
"""
|
|
78
|
-
# Check if we have at least one update for each instrument
|
|
79
|
-
if not self._ohlcvs:
|
|
80
|
-
return False
|
|
81
|
-
|
|
82
|
-
return all(instrument in self._ready_instruments for instrument in self._ohlcvs)
|
|
83
|
-
|
|
84
72
|
@SW.watch("CachedMarketDataHolder")
|
|
85
73
|
def get_ohlcv(self, instrument: Instrument, timeframe: str | None = None, max_size: float | int = np.inf) -> OHLCV:
|
|
86
74
|
tf = convert_tf_str_td64(timeframe) if timeframe else self.default_timeframe
|
|
@@ -121,9 +109,6 @@ class CachedMarketDataHolder:
|
|
|
121
109
|
if event_type != DataType.OHLC:
|
|
122
110
|
self._instr_to_sub_to_buffer[instrument][event_type].append(data)
|
|
123
111
|
|
|
124
|
-
if not is_historical and is_base_data:
|
|
125
|
-
self._ready_instruments.add(instrument)
|
|
126
|
-
|
|
127
112
|
if not update_ohlc:
|
|
128
113
|
return
|
|
129
114
|
|
qubx/core/metrics.py
CHANGED
|
@@ -21,7 +21,7 @@ from statsmodels.regression.linear_model import OLS
|
|
|
21
21
|
from qubx import logger
|
|
22
22
|
from qubx.core.basics import Instrument
|
|
23
23
|
from qubx.core.series import OHLCV
|
|
24
|
-
from qubx.pandaz.utils import ohlc_resample
|
|
24
|
+
from qubx.pandaz.utils import ohlc_resample, srows
|
|
25
25
|
from qubx.utils.charting.lookinglass import LookingGlass
|
|
26
26
|
from qubx.utils.charting.mpl_helpers import sbp
|
|
27
27
|
from qubx.utils.misc import makedirs, version
|
|
@@ -1500,6 +1500,9 @@ def get_symbol_pnls(
|
|
|
1500
1500
|
|
|
1501
1501
|
|
|
1502
1502
|
def combine_sessions(sessions: list[TradingSessionResult], name: str = "Portfolio") -> TradingSessionResult:
|
|
1503
|
+
"""
|
|
1504
|
+
DEPRECATED: use extend_trading_results instead
|
|
1505
|
+
"""
|
|
1503
1506
|
session = copy(sessions[0])
|
|
1504
1507
|
session.name = name
|
|
1505
1508
|
session.instruments = list(set(chain.from_iterable([e.instruments for e in sessions])))
|
|
@@ -1518,6 +1521,45 @@ def combine_sessions(sessions: list[TradingSessionResult], name: str = "Portfoli
|
|
|
1518
1521
|
return session
|
|
1519
1522
|
|
|
1520
1523
|
|
|
1524
|
+
def extend_trading_results(results: list[TradingSessionResult]) -> TradingSessionResult:
|
|
1525
|
+
"""
|
|
1526
|
+
Combine multiple trading session results into a single result by extending the sessions.
|
|
1527
|
+
"""
|
|
1528
|
+
import os
|
|
1529
|
+
|
|
1530
|
+
pfls, execs, exch, names, instrs, clss = [], [], [], [], [], []
|
|
1531
|
+
cap = 0.0
|
|
1532
|
+
|
|
1533
|
+
for b in sorted(results, key=lambda x: x.start):
|
|
1534
|
+
pfls.append(b.portfolio_log)
|
|
1535
|
+
execs.append(b.executions_log)
|
|
1536
|
+
exch.extend(b.exchanges)
|
|
1537
|
+
names.append(b.name)
|
|
1538
|
+
cap += b.capital if isinstance(b.capital, float) else 0.0 # TODO: add handling dict
|
|
1539
|
+
instrs.extend(b.instruments)
|
|
1540
|
+
clss.append(b.strategy_class)
|
|
1541
|
+
cmn = os.path.commonprefix(names)
|
|
1542
|
+
names = [x[len(cmn) :] for x in names]
|
|
1543
|
+
f_pfls: pd.DataFrame = srows(*pfls, keep="last") # type: ignore
|
|
1544
|
+
f_execs: pd.DataFrame = srows(*execs, keep="last") # type: ignore
|
|
1545
|
+
r = TradingSessionResult(
|
|
1546
|
+
0,
|
|
1547
|
+
cmn + "-".join(names),
|
|
1548
|
+
start=f_pfls.index[0],
|
|
1549
|
+
stop=f_pfls.index[-1],
|
|
1550
|
+
exchanges=list(set(exch)),
|
|
1551
|
+
capital=cap / len(results), # average capital ???
|
|
1552
|
+
instruments=list(set(instrs)),
|
|
1553
|
+
base_currency=results[0].base_currency,
|
|
1554
|
+
commissions=results[0].commissions, # what if different commissions ???
|
|
1555
|
+
portfolio_log=f_pfls,
|
|
1556
|
+
executions_log=f_execs,
|
|
1557
|
+
signals_log=pd.DataFrame(),
|
|
1558
|
+
strategy_class="-".join(set(clss)), # what if different strategy classes ???
|
|
1559
|
+
)
|
|
1560
|
+
return r
|
|
1561
|
+
|
|
1562
|
+
|
|
1521
1563
|
def _plt_to_base64() -> str:
|
|
1522
1564
|
fig = plt.gcf()
|
|
1523
1565
|
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -66,6 +66,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
66
66
|
_pool: ThreadPool | None
|
|
67
67
|
_trig_bar_freq_nsec: int | None = None
|
|
68
68
|
_cur_sim_step: int | None = None
|
|
69
|
+
_updated_instruments: set[Instrument] = set()
|
|
69
70
|
|
|
70
71
|
def __init__(
|
|
71
72
|
self,
|
|
@@ -109,6 +110,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
109
110
|
}
|
|
110
111
|
self._strategy_name = strategy.__class__.__name__
|
|
111
112
|
self._trig_bar_freq_nsec = None
|
|
113
|
+
self._updated_instruments = set()
|
|
112
114
|
|
|
113
115
|
def set_fit_schedule(self, schedule: str) -> None:
|
|
114
116
|
rule = process_schedule_spec(schedule)
|
|
@@ -340,6 +342,12 @@ class ProcessingManager(IProcessingManager):
|
|
|
340
342
|
_d_probe,
|
|
341
343
|
)
|
|
342
344
|
|
|
345
|
+
def _is_data_ready(self) -> bool:
|
|
346
|
+
"""
|
|
347
|
+
Check if at least one update was received for all instruments in the context.
|
|
348
|
+
"""
|
|
349
|
+
return all(instrument in self._updated_instruments for instrument in self._context.instruments)
|
|
350
|
+
|
|
343
351
|
def __update_base_data(
|
|
344
352
|
self, instrument: Instrument, event_type: str, data: Timestamped, is_historical: bool = False
|
|
345
353
|
) -> bool:
|
|
@@ -366,6 +374,9 @@ class ProcessingManager(IProcessingManager):
|
|
|
366
374
|
# update trackers, gatherers on base data
|
|
367
375
|
if not is_historical:
|
|
368
376
|
if is_base_data:
|
|
377
|
+
# - mark instrument as updated
|
|
378
|
+
self._updated_instruments.add(instrument)
|
|
379
|
+
|
|
369
380
|
self._account.update_position_price(self._time_provider.time(), instrument, _update)
|
|
370
381
|
target_positions = self.__process_and_log_target_positions(
|
|
371
382
|
self._position_tracker.update(self._context, instrument, _update)
|
|
@@ -421,13 +432,13 @@ class ProcessingManager(IProcessingManager):
|
|
|
421
432
|
pass
|
|
422
433
|
|
|
423
434
|
def _handle_start(self) -> None:
|
|
424
|
-
if not self.
|
|
435
|
+
if not self._is_data_ready():
|
|
425
436
|
return
|
|
426
437
|
self._strategy.on_start(self._context)
|
|
427
438
|
self._context._strategy_state.is_on_start_called = True
|
|
428
439
|
|
|
429
440
|
def _handle_state_resolution(self) -> None:
|
|
430
|
-
if not self.
|
|
441
|
+
if not self._is_data_ready():
|
|
431
442
|
return
|
|
432
443
|
|
|
433
444
|
resolver = self._context.initializer.get_state_resolver()
|
|
@@ -448,7 +459,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
448
459
|
resolver(self._context, self._context.get_warmup_positions(), self._context.get_warmup_orders())
|
|
449
460
|
|
|
450
461
|
def _handle_warmup_finished(self) -> None:
|
|
451
|
-
if not self.
|
|
462
|
+
if not self._is_data_ready():
|
|
452
463
|
return
|
|
453
464
|
self._strategy.on_warmup_finished(self._context)
|
|
454
465
|
self._context._strategy_state.is_on_warmup_finished_called = True
|
|
@@ -457,7 +468,7 @@ class ProcessingManager(IProcessingManager):
|
|
|
457
468
|
"""
|
|
458
469
|
When scheduled fit event is happened - we need to invoke strategy on_fit method
|
|
459
470
|
"""
|
|
460
|
-
if not self.
|
|
471
|
+
if not self._is_data_ready():
|
|
461
472
|
return
|
|
462
473
|
self._fit_is_running = True
|
|
463
474
|
self._run_in_thread_pool(self.__invoke_on_fit)
|
|
Binary file
|
|
Binary file
|
qubx/data/tardis.py
CHANGED
|
@@ -81,21 +81,25 @@ class TardisCsvDataReader(DataReader):
|
|
|
81
81
|
_filt_files = [file for file in _files if t_0 <= file.stem.split("_")[0] <= t_1]
|
|
82
82
|
|
|
83
83
|
tables = []
|
|
84
|
-
fieldnames = None
|
|
84
|
+
# fieldnames = None
|
|
85
85
|
for f_path in _filt_files:
|
|
86
|
-
table =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
86
|
+
table = pd.read_csv(f_path)
|
|
87
|
+
tables.append(table)
|
|
88
|
+
# table = csv.read_csv(
|
|
89
|
+
# f_path,
|
|
90
|
+
# parse_options=csv.ParseOptions(ignore_empty_lines=True),
|
|
91
|
+
# )
|
|
92
|
+
# if not fieldnames:
|
|
93
|
+
# fieldnames = table.column_names
|
|
94
|
+
# tables.append(table.to_pandas())
|
|
95
|
+
|
|
96
|
+
return pd.concat(tables)
|
|
97
|
+
|
|
98
|
+
# transform.start_transform(data_id, fieldnames or [], start=start, stop=stop)
|
|
99
|
+
# raw_data = pd.concat(tables).to_numpy()
|
|
100
|
+
# transform.process_data(raw_data)
|
|
101
|
+
|
|
102
|
+
# return transform.collect()
|
|
99
103
|
|
|
100
104
|
def get_exchanges(self) -> list[str]:
|
|
101
105
|
return [exchange.name for exchange in self.path.iterdir() if exchange.is_dir()]
|
qubx/notifications/__init__.py
CHANGED
|
@@ -7,5 +7,13 @@ for various notification channels.
|
|
|
7
7
|
|
|
8
8
|
from .composite import CompositeLifecycleNotifier
|
|
9
9
|
from .slack import SlackLifecycleNotifier
|
|
10
|
+
from .throttler import CountBasedThrottler, IMessageThrottler, NoThrottling, TimeWindowThrottler
|
|
10
11
|
|
|
11
|
-
__all__ = [
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CompositeLifecycleNotifier",
|
|
14
|
+
"SlackLifecycleNotifier",
|
|
15
|
+
"IMessageThrottler",
|
|
16
|
+
"TimeWindowThrottler",
|
|
17
|
+
"CountBasedThrottler",
|
|
18
|
+
"NoThrottling"
|
|
19
|
+
]
|
qubx/notifications/slack.py
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Slack
|
|
2
|
+
Slack notifications for strategy lifecycle events.
|
|
3
3
|
|
|
4
|
-
This module provides
|
|
4
|
+
This module provides a Slack implementation of IStrategyLifecycleNotifier.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import datetime
|
|
8
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any
|
|
10
10
|
|
|
11
11
|
import requests
|
|
12
12
|
|
|
13
13
|
from qubx import logger
|
|
14
14
|
from qubx.core.interfaces import IStrategyLifecycleNotifier
|
|
15
|
+
from qubx.notifications.throttler import IMessageThrottler, NoThrottling
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
@@ -30,6 +31,7 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
30
31
|
emoji_stop: str = ":checkered_flag:",
|
|
31
32
|
emoji_error: str = ":rotating_light:",
|
|
32
33
|
max_workers: int = 1,
|
|
34
|
+
throttler: IMessageThrottler | None = None,
|
|
33
35
|
):
|
|
34
36
|
"""
|
|
35
37
|
Initialize the Slack Lifecycle Notifier.
|
|
@@ -40,18 +42,28 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
40
42
|
emoji_start: Emoji to use for start events
|
|
41
43
|
emoji_stop: Emoji to use for stop events
|
|
42
44
|
emoji_error: Emoji to use for error events
|
|
45
|
+
max_workers: Number of worker threads for posting messages
|
|
46
|
+
throttler: Optional message throttler to prevent flooding
|
|
43
47
|
"""
|
|
44
48
|
self._webhook_url = webhook_url
|
|
45
49
|
self._environment = environment
|
|
46
50
|
self._emoji_start = emoji_start
|
|
47
51
|
self._emoji_stop = emoji_stop
|
|
48
52
|
self._emoji_error = emoji_error
|
|
53
|
+
self._throttler = throttler if throttler is not None else NoThrottling()
|
|
49
54
|
|
|
50
55
|
self._executor = ThreadPoolExecutor(max_workers=max_workers, thread_name_prefix="slack_notifier")
|
|
51
56
|
|
|
52
57
|
logger.info(f"[SlackLifecycleNotifier] Initialized for environment '{environment}'")
|
|
53
58
|
|
|
54
|
-
def _post_to_slack(
|
|
59
|
+
def _post_to_slack(
|
|
60
|
+
self,
|
|
61
|
+
message: str,
|
|
62
|
+
emoji: str,
|
|
63
|
+
color: str,
|
|
64
|
+
metadata: dict[str, Any] | None = None,
|
|
65
|
+
throttle_key: str | None = None,
|
|
66
|
+
) -> None:
|
|
55
67
|
"""
|
|
56
68
|
Submit a notification to be posted to Slack by the worker thread.
|
|
57
69
|
|
|
@@ -60,15 +72,26 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
60
72
|
emoji: Emoji to use in the message
|
|
61
73
|
color: Color for the message attachment
|
|
62
74
|
metadata: Optional dictionary with additional fields to include
|
|
75
|
+
throttle_key: Optional key for throttling (if None, no throttling is applied)
|
|
63
76
|
"""
|
|
64
77
|
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
|
+
|
|
65
83
|
# Submit the task to the executor
|
|
66
|
-
self._executor.submit(self._post_to_slack_impl, message, emoji, color, metadata)
|
|
84
|
+
self._executor.submit(self._post_to_slack_impl, message, emoji, color, metadata, throttle_key)
|
|
67
85
|
except Exception as e:
|
|
68
86
|
logger.error(f"[SlackLifecycleNotifier] Failed to queue Slack message: {e}")
|
|
69
87
|
|
|
70
88
|
def _post_to_slack_impl(
|
|
71
|
-
self,
|
|
89
|
+
self,
|
|
90
|
+
message: str,
|
|
91
|
+
emoji: str,
|
|
92
|
+
color: str,
|
|
93
|
+
metadata: dict[str, Any] | None = None,
|
|
94
|
+
throttle_key: str | None = None,
|
|
72
95
|
) -> bool:
|
|
73
96
|
"""
|
|
74
97
|
Implementation that actually posts to Slack (called from worker thread).
|
|
@@ -78,6 +101,7 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
78
101
|
emoji: Emoji to use in the message
|
|
79
102
|
color: Color for the message attachment
|
|
80
103
|
metadata: Optional dictionary with additional fields to include
|
|
104
|
+
throttle_key: Optional key used for throttling
|
|
81
105
|
|
|
82
106
|
Returns:
|
|
83
107
|
bool: True if the post was successful, False otherwise
|
|
@@ -107,13 +131,18 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
107
131
|
|
|
108
132
|
response = requests.post(self._webhook_url, json=data)
|
|
109
133
|
response.raise_for_status()
|
|
134
|
+
|
|
135
|
+
# Register that we sent the message (for throttling)
|
|
136
|
+
if throttle_key is not None:
|
|
137
|
+
self._throttler.register_sent(throttle_key)
|
|
138
|
+
|
|
110
139
|
logger.debug(f"[SlackLifecycleNotifier] Successfully posted message: {message}")
|
|
111
140
|
return True
|
|
112
141
|
except requests.RequestException as e:
|
|
113
142
|
logger.error(f"[SlackLifecycleNotifier] Failed to post to Slack: {e}")
|
|
114
143
|
return False
|
|
115
144
|
|
|
116
|
-
def notify_start(self, strategy_name: str, metadata:
|
|
145
|
+
def notify_start(self, strategy_name: str, metadata: dict[str, Any] | None = None) -> None:
|
|
117
146
|
"""
|
|
118
147
|
Notify that a strategy has started.
|
|
119
148
|
|
|
@@ -128,7 +157,7 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
128
157
|
except Exception as e:
|
|
129
158
|
logger.error(f"[SlackLifecycleNotifier] Failed to notify start: {e}")
|
|
130
159
|
|
|
131
|
-
def notify_stop(self, strategy_name: str, metadata:
|
|
160
|
+
def notify_stop(self, strategy_name: str, metadata: dict[str, Any] | None = None) -> None:
|
|
132
161
|
"""
|
|
133
162
|
Notify that a strategy has stopped.
|
|
134
163
|
|
|
@@ -143,7 +172,7 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
143
172
|
except Exception as e:
|
|
144
173
|
logger.error(f"[SlackLifecycleNotifier] Failed to notify stop: {e}")
|
|
145
174
|
|
|
146
|
-
def notify_error(self, strategy_name: str, error: Exception, metadata:
|
|
175
|
+
def notify_error(self, strategy_name: str, error: Exception, metadata: dict[str, Any] | None = None) -> None:
|
|
147
176
|
"""
|
|
148
177
|
Notify that a strategy has encountered an error.
|
|
149
178
|
|
|
@@ -161,7 +190,11 @@ class SlackLifecycleNotifier(IStrategyLifecycleNotifier):
|
|
|
161
190
|
metadata["Error Message"] = str(error)
|
|
162
191
|
|
|
163
192
|
message = f"[{strategy_name}] ALERT: Strategy error in {self._environment}"
|
|
164
|
-
|
|
193
|
+
|
|
194
|
+
# Create a throttle key for this strategy/error type combination
|
|
195
|
+
throttle_key = f"error:{strategy_name}:{type(error).__name__}"
|
|
196
|
+
|
|
197
|
+
self._post_to_slack(message, self._emoji_error, "#FF0000", metadata, throttle_key=throttle_key)
|
|
165
198
|
logger.debug(f"[SlackLifecycleNotifier] Queued error notification for {strategy_name}")
|
|
166
199
|
except Exception as e:
|
|
167
200
|
logger.error(f"[SlackLifecycleNotifier] Failed to notify error: {e}")
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message Throttling for Notifications.
|
|
3
|
+
|
|
4
|
+
This module defines interfaces and implementations for throttling
|
|
5
|
+
notification messages to prevent flooding notification channels.
|
|
6
|
+
|
|
7
|
+
Usage Examples:
|
|
8
|
+
1. Basic TimeWindowThrottler with default settings (allows 1 message per key per 10 seconds):
|
|
9
|
+
```python
|
|
10
|
+
from qubx.notifications.throttler import TimeWindowThrottler
|
|
11
|
+
|
|
12
|
+
throttler = TimeWindowThrottler()
|
|
13
|
+
if throttler.should_send("error:mystrategy:ValueError"):
|
|
14
|
+
# Send the message
|
|
15
|
+
send_message()
|
|
16
|
+
# Update the throttler
|
|
17
|
+
throttler.register_sent("error:mystrategy:ValueError")
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
2. CountBasedThrottler (allows up to N messages per key within a time window):
|
|
21
|
+
```python
|
|
22
|
+
from qubx.notifications.throttler import CountBasedThrottler
|
|
23
|
+
|
|
24
|
+
# Allow up to 5 messages per minute for each key
|
|
25
|
+
throttler = CountBasedThrottler(max_count=5, window_seconds=60.0)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
3. In a configuration file for SlackLifecycleNotifier:
|
|
29
|
+
```yaml
|
|
30
|
+
notifiers:
|
|
31
|
+
- notifier: SlackLifecycleNotifier
|
|
32
|
+
parameters:
|
|
33
|
+
webhook_url: ${SLACK_WEBHOOK_URL}
|
|
34
|
+
environment: production
|
|
35
|
+
throttle:
|
|
36
|
+
type: TimeWindow
|
|
37
|
+
window_seconds: 30.0
|
|
38
|
+
```
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import time
|
|
42
|
+
from abc import ABC, abstractmethod
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class IMessageThrottler(ABC):
|
|
46
|
+
"""Interface for message throttlers that can limit the frequency of notifications."""
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def should_send(self, key: str) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Determine if a message with the given key should be sent based on throttling rules.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
key: A unique identifier for the type of message being sent
|
|
55
|
+
(e.g., "error:{strategy_name}")
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
bool: True if the message should be sent, False if it should be throttled
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def register_sent(self, key: str) -> None:
|
|
64
|
+
"""
|
|
65
|
+
Register that a message with the given key was sent.
|
|
66
|
+
This updates the internal state of the throttler.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
key: A unique identifier for the type of message that was sent
|
|
70
|
+
"""
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class TimeWindowThrottler(IMessageThrottler):
|
|
75
|
+
"""
|
|
76
|
+
Throttles messages based on a time window.
|
|
77
|
+
|
|
78
|
+
Only allows one message per key within a specified time window.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, window_seconds: float = 10.0):
|
|
82
|
+
"""
|
|
83
|
+
Initialize the time window throttler.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
window_seconds: Minimum time between messages with the same key, in seconds
|
|
87
|
+
"""
|
|
88
|
+
self._window_seconds = window_seconds
|
|
89
|
+
self._last_sent_times: dict[str, float] = {}
|
|
90
|
+
|
|
91
|
+
def should_send(self, key: str) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Check if a message with the given key should be sent based on the time window.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
key: Message key to check
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
bool: True if enough time has passed since the last message with this key
|
|
100
|
+
"""
|
|
101
|
+
current_time = time.time()
|
|
102
|
+
last_sent = self._last_sent_times.get(key, 0)
|
|
103
|
+
return (current_time - last_sent) >= self._window_seconds
|
|
104
|
+
|
|
105
|
+
def register_sent(self, key: str) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Register that a message with the given key was sent.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
key: Key of the message that was sent
|
|
111
|
+
"""
|
|
112
|
+
self._last_sent_times[key] = time.time()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class CountBasedThrottler(IMessageThrottler):
|
|
116
|
+
"""
|
|
117
|
+
Throttles messages based on a count within a time window.
|
|
118
|
+
|
|
119
|
+
Allows a specified number of messages per key within a time window.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self, max_count: int = 3, window_seconds: float = 60.0):
|
|
123
|
+
"""
|
|
124
|
+
Initialize the count-based throttler.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
max_count: Maximum number of messages allowed in the time window
|
|
128
|
+
window_seconds: Time window in seconds
|
|
129
|
+
"""
|
|
130
|
+
self._max_count = max_count
|
|
131
|
+
self._window_seconds = window_seconds
|
|
132
|
+
self._message_history: dict[str, list[float]] = {}
|
|
133
|
+
|
|
134
|
+
def should_send(self, key: str) -> bool:
|
|
135
|
+
"""
|
|
136
|
+
Check if a message with the given key should be sent based on the count limit.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
key: Message key to check
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
bool: True if the message count is below the limit
|
|
143
|
+
"""
|
|
144
|
+
current_time = time.time()
|
|
145
|
+
|
|
146
|
+
# Initialize history for this key if it doesn't exist
|
|
147
|
+
if key not in self._message_history:
|
|
148
|
+
self._message_history[key] = []
|
|
149
|
+
|
|
150
|
+
# Remove timestamps older than the window
|
|
151
|
+
self._message_history[key] = [
|
|
152
|
+
ts for ts in self._message_history[key] if (current_time - ts) < self._window_seconds
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
# Check if we're under the message count limit
|
|
156
|
+
return len(self._message_history[key]) < self._max_count
|
|
157
|
+
|
|
158
|
+
def register_sent(self, key: str) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Register that a message with the given key was sent.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
key: Key of the message that was sent
|
|
164
|
+
"""
|
|
165
|
+
current_time = time.time()
|
|
166
|
+
|
|
167
|
+
if key not in self._message_history:
|
|
168
|
+
self._message_history[key] = []
|
|
169
|
+
|
|
170
|
+
self._message_history[key].append(current_time)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class NoThrottling(IMessageThrottler):
|
|
174
|
+
"""A throttler implementation that doesn't actually throttle - allows all messages."""
|
|
175
|
+
|
|
176
|
+
def should_send(self, key: str) -> bool:
|
|
177
|
+
"""Always returns True, allowing all messages to be sent."""
|
|
178
|
+
return True
|
|
179
|
+
|
|
180
|
+
def register_sent(self, key: str) -> None:
|
|
181
|
+
"""No-op implementation."""
|
|
182
|
+
pass
|
qubx/pandaz/utils.py
CHANGED
|
@@ -23,7 +23,10 @@ def check_frame_columns(x, *args):
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def rolling_forward_test_split(
|
|
26
|
-
x: pd.Series | pd.DataFrame,
|
|
26
|
+
x: pd.Series | pd.DataFrame,
|
|
27
|
+
training_period: int,
|
|
28
|
+
test_period: int,
|
|
29
|
+
units: str | None = None,
|
|
27
30
|
):
|
|
28
31
|
"""
|
|
29
32
|
Split data into training and testing **rolling** periods.
|
|
@@ -51,7 +54,7 @@ def rolling_forward_test_split(
|
|
|
51
54
|
:return:
|
|
52
55
|
"""
|
|
53
56
|
# unit formats from pd.TimeDelta and formats for pd.resample
|
|
54
|
-
units_format = {"H": "
|
|
57
|
+
units_format = {"H": "h", "D": "d", "W": "W", "M": "MS", "Q": "QS", "Y": "AS", "MIN": "min"}
|
|
55
58
|
|
|
56
59
|
if units:
|
|
57
60
|
if units.upper() not in units_format:
|
|
Binary file
|
qubx/utils/runner/factory.py
CHANGED
|
@@ -259,7 +259,7 @@ def create_lifecycle_notifiers(
|
|
|
259
259
|
Create lifecycle notifiers from the configuration.
|
|
260
260
|
|
|
261
261
|
Args:
|
|
262
|
-
|
|
262
|
+
notifiers: List of notifier configurations
|
|
263
263
|
strategy_name: Name of the strategy
|
|
264
264
|
|
|
265
265
|
Returns:
|
|
@@ -283,6 +283,57 @@ def create_lifecycle_notifiers(
|
|
|
283
283
|
for key, value in notifier_config.parameters.items():
|
|
284
284
|
params[key] = resolve_env_vars(value)
|
|
285
285
|
|
|
286
|
+
# Create throttler if configured or use default TimeWindowThrottler
|
|
287
|
+
if "SlackLifecycleNotifier" in notifier_class_name and "throttler" not in params:
|
|
288
|
+
# Import here to avoid circular imports
|
|
289
|
+
from qubx.notifications.throttler import TimeWindowThrottler
|
|
290
|
+
|
|
291
|
+
# Create default throttler with 10s window
|
|
292
|
+
default_window = 10.0
|
|
293
|
+
params["throttler"] = TimeWindowThrottler(window_seconds=default_window)
|
|
294
|
+
logger.info(
|
|
295
|
+
f"Using default TimeWindowThrottler with window={default_window}s for {notifier_class_name}"
|
|
296
|
+
)
|
|
297
|
+
elif "throttle" in params:
|
|
298
|
+
throttle_config = params.pop("throttle")
|
|
299
|
+
|
|
300
|
+
if isinstance(throttle_config, dict):
|
|
301
|
+
throttler_type = throttle_config.get("type", "TimeWindow")
|
|
302
|
+
window_seconds = float(throttle_config.get("window_seconds", 10.0))
|
|
303
|
+
max_count = int(throttle_config.get("max_count", 3))
|
|
304
|
+
|
|
305
|
+
if throttler_type.lower() == "timewindow":
|
|
306
|
+
from qubx.notifications.throttler import TimeWindowThrottler
|
|
307
|
+
|
|
308
|
+
throttler = TimeWindowThrottler(window_seconds=window_seconds)
|
|
309
|
+
logger.info(f"Created TimeWindowThrottler with window_seconds={window_seconds}")
|
|
310
|
+
elif throttler_type.lower() == "countbased":
|
|
311
|
+
from qubx.notifications.throttler import CountBasedThrottler
|
|
312
|
+
|
|
313
|
+
throttler = CountBasedThrottler(max_count=max_count, window_seconds=window_seconds)
|
|
314
|
+
logger.info(
|
|
315
|
+
f"Created CountBasedThrottler with max_count={max_count}, window_seconds={window_seconds}"
|
|
316
|
+
)
|
|
317
|
+
elif throttler_type.lower() == "none":
|
|
318
|
+
from qubx.notifications.throttler import NoThrottling
|
|
319
|
+
|
|
320
|
+
throttler = NoThrottling()
|
|
321
|
+
logger.info("Created NoThrottling throttler")
|
|
322
|
+
else:
|
|
323
|
+
logger.warning(f"Unknown throttler type '{throttler_type}', defaulting to TimeWindowThrottler")
|
|
324
|
+
from qubx.notifications.throttler import TimeWindowThrottler
|
|
325
|
+
|
|
326
|
+
throttler = TimeWindowThrottler(window_seconds=window_seconds)
|
|
327
|
+
|
|
328
|
+
params["throttler"] = throttler
|
|
329
|
+
elif isinstance(throttle_config, (int, float)):
|
|
330
|
+
# Simple case: just a window_seconds value
|
|
331
|
+
from qubx.notifications.throttler import TimeWindowThrottler
|
|
332
|
+
|
|
333
|
+
throttler = TimeWindowThrottler(window_seconds=float(throttle_config))
|
|
334
|
+
logger.info(f"Created TimeWindowThrottler with window_seconds={throttle_config}")
|
|
335
|
+
params["throttler"] = throttler
|
|
336
|
+
|
|
286
337
|
# Create the notifier instance
|
|
287
338
|
notifier = notifier_class(**params)
|
|
288
339
|
_notifiers.append(notifier)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
qubx/__init__.py,sha256=GBvbyDpm2yCMJVmGW66Jo0giLOUsKKldDGcVA_r9Ohc,8294
|
|
2
|
-
qubx/_nb_magic.py,sha256=
|
|
2
|
+
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
|
|
@@ -9,7 +9,7 @@ qubx/backtester/ome.py,sha256=Uf3wqyVjUEpm1jrDQ4PE77E3R3B7wJJy1vaOOmvQqWg,15610
|
|
|
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
|
-
qubx/backtester/simulated_exchange.py,sha256=
|
|
12
|
+
qubx/backtester/simulated_exchange.py,sha256=Xg0yv21gq4q9CeCeZoupcenNEBORrxpb93ONZEGL2xk,8076
|
|
13
13
|
qubx/backtester/simulator.py,sha256=cSbW42X-YlAutZlOQ3Y4mAJWXr_1WomYprtWZVMe3Uk,9225
|
|
14
14
|
qubx/backtester/utils.py,sha256=nHrgKcIkyp5gz8wrPwMp1fRItUtQfvOPjxZhcaCwN-o,32729
|
|
15
15
|
qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -40,23 +40,23 @@ qubx/core/context.py,sha256=ZetAOnQ9pfUiMAFln02zJr4FePTjKaY5OSIVwaMPhnE,22822
|
|
|
40
40
|
qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
|
|
41
41
|
qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
|
|
42
42
|
qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
|
|
43
|
-
qubx/core/helpers.py,sha256=
|
|
43
|
+
qubx/core/helpers.py,sha256=m7JrZaBckXHb4zjhKpdCbxFe3kfda-SCNLfagyq7Ve4,19158
|
|
44
44
|
qubx/core/initializer.py,sha256=PUiD_cIjvGpuPjYyRpUjpwm3xNQ2Kipa8bAhbtxCQRo,3935
|
|
45
45
|
qubx/core/interfaces.py,sha256=CzIl8tB6ImQkDcZEmhpstwHPOCY8NhZxXmBHLQUAieI,58253
|
|
46
46
|
qubx/core/loggers.py,sha256=0g33jfipGFShSMrXBoYVzL0GfTzI36mwBJqHNUHmhdo,13342
|
|
47
47
|
qubx/core/lookups.py,sha256=n5ZjjEhhRvmidCB-Cubr1b0Opm6lf_QVZNEWa_BOQG0,19376
|
|
48
|
-
qubx/core/metrics.py,sha256=
|
|
48
|
+
qubx/core/metrics.py,sha256=IUTVDfO7HW68GGoLsj6kkLz8ueg-bnXRiWflIts8K_w,60264
|
|
49
49
|
qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
|
|
50
50
|
qubx/core/mixins/market.py,sha256=lBappEimPhIuI0vmUvwVlIztkYjlEjJBpP-AdpfudII,3948
|
|
51
|
-
qubx/core/mixins/processing.py,sha256=
|
|
51
|
+
qubx/core/mixins/processing.py,sha256=cmjD0PcQv3gFP6oILfNgdNgw7Tez0fUZu_nFn6680VI,24979
|
|
52
52
|
qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
|
|
53
53
|
qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
|
|
54
54
|
qubx/core/mixins/universe.py,sha256=L3s2Jw46_J1iDh4622Gk_LvCjol4W7mflBwEHrLfZEw,9899
|
|
55
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
55
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=kaqGuSydE041VcwqUyHpIybK27yE1g-INOtDP1zfI8Q,978280
|
|
56
56
|
qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
|
|
57
57
|
qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
|
|
58
58
|
qubx/core/series.pyx,sha256=7cM3zZThW59waHiYcZmMxvYj-HYD7Ej_l7nKA4emPjE,46477
|
|
59
|
-
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
59
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=LWgrhKGrBQ7wFwPPHwZgjqPZxcRLzq81z0x3B1iiBE0,86568
|
|
60
60
|
qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
|
|
61
61
|
qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
|
|
62
62
|
qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
|
|
@@ -65,7 +65,7 @@ qubx/data/helpers.py,sha256=VcXBl1kfWzAOqrjadKrP9WemGjJIB0q3xascbesErh4,16268
|
|
|
65
65
|
qubx/data/hft.py,sha256=be7AwzTOjqqCENn0ClrZoHDyKv3SFG66IyTp8QadHlM,33687
|
|
66
66
|
qubx/data/readers.py,sha256=H68n38VLMjjk8R5FW7URGLcJCh0MREKFGdMGgWCWzhU,62503
|
|
67
67
|
qubx/data/registry.py,sha256=45mjy5maBSO6cf-0zfIRRDs8b0VDW7wHSPn43aRjv-o,3883
|
|
68
|
-
qubx/data/tardis.py,sha256=
|
|
68
|
+
qubx/data/tardis.py,sha256=O-zglpusmO6vCY3arSOgH6KUbkfPajSAIQfMKlVmh_E,33878
|
|
69
69
|
qubx/emitters/__init__.py,sha256=tpJ9OoW-gycTBXGJ0647tT8-dVBmq23T2wMX_kmk3nM,565
|
|
70
70
|
qubx/emitters/base.py,sha256=z0CiEnIGkizd-4Btvq9Auxg3BpnkKN6M8-ksAH2enQc,7745
|
|
71
71
|
qubx/emitters/composite.py,sha256=8DsPIUtaJ95Oww9QTVVB6LR7Wcb6TJ-c1jIHMGuttz4,2784
|
|
@@ -96,12 +96,13 @@ qubx/loggers/inmemory.py,sha256=49Y-jsRxDzBLWQdQMIKjVTvnx_79EbjFpHwj3v8Mgno,2642
|
|
|
96
96
|
qubx/loggers/mongo.py,sha256=dOEpCcIxT6O9MgpK2srpzxyuto6QaQgTxMK0WcEIR70,2703
|
|
97
97
|
qubx/math/__init__.py,sha256=ltHSQj40sCBm3owcvtoZp34h6ws7pZCFcSZgUkTsUCY,114
|
|
98
98
|
qubx/math/stats.py,sha256=uXm4NpBRxuHFTjXERv8rjM0MAJof8zr1Cklyra4CcBA,4056
|
|
99
|
-
qubx/notifications/__init__.py,sha256=
|
|
99
|
+
qubx/notifications/__init__.py,sha256=cb3DGxuiA8UwSTlTeF5pQKy4-vBef3gMeKtfcxEjdN4,547
|
|
100
100
|
qubx/notifications/composite.py,sha256=fa-rvHEn6k-Fma5N7cT-7Sk7hzVyB0KDs2ktDyoyLxM,2689
|
|
101
|
-
qubx/notifications/slack.py,sha256=
|
|
101
|
+
qubx/notifications/slack.py,sha256=FZ0zfTA-zRHOsOsIVBtM7mkt4m-adiuiV5yrwliS9RM,7976
|
|
102
|
+
qubx/notifications/throttler.py,sha256=8jnymPQbrgtN1rD7REQa2sA9teSWTqkk_uT9oaknOyc,5618
|
|
102
103
|
qubx/pandaz/__init__.py,sha256=6BYz6gSgxjNa7WP1XqWflYG7WIq1ppSD9h1XGR5M5YQ,682
|
|
103
104
|
qubx/pandaz/ta.py,sha256=SjK3jORf6Q3XYBwTixXnjK5elbzdvT14WR2OJdQXo20,91666
|
|
104
|
-
qubx/pandaz/utils.py,sha256=
|
|
105
|
+
qubx/pandaz/utils.py,sha256=f7airZjUKHkhKICL0HF_R9Vvcswdgef7Yr35Xb_Ppzs,23407
|
|
105
106
|
qubx/resources/_build.py,sha256=XE7XNuDqfXPc2OriLobKXmPMvwa7Z8AKAD-18fnf0e4,8802
|
|
106
107
|
qubx/resources/instruments/symbols-binance.cm.json,sha256=rNI3phNeeRY95_IE7_0Um9d5U4jUtEijZQ_PaYg5cdw,25127
|
|
107
108
|
qubx/resources/instruments/symbols-binance.json,sha256=Qx_XckgsWNhmmV8_t5DpG0AeGkuTyt1uiif2EeeBDIg,939678
|
|
@@ -122,7 +123,7 @@ qubx/restorers/signal.py,sha256=9TAaJOEKPjZXuciFFVn6Z8a-Z8CfVSjRGFRcwEgbPLY,1074
|
|
|
122
123
|
qubx/restorers/state.py,sha256=dLaVnUwRCNRkUqbYyi0RfZs3Q3AdglkI_qTtQ8GDD5Y,7289
|
|
123
124
|
qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
|
|
124
125
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
125
|
-
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
126
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=CCvVqxcz64fK-Am57qTJzknn3CNYY-8PFEHlRgj7RTI,654440
|
|
126
127
|
qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
|
|
127
128
|
qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
|
|
128
129
|
qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
|
|
@@ -155,12 +156,12 @@ qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
|
|
|
155
156
|
qubx/utils/runner/_jupyter_runner.pyt,sha256=fDj4AUs25jsdGmY9DDeSFufH1JkVhLFwy0BOmVO7nIU,9609
|
|
156
157
|
qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
|
|
157
158
|
qubx/utils/runner/configs.py,sha256=nxIelzfHtv7GagkEHBJ6mRm_30jmBa6pSPujL-k0uqo,3749
|
|
158
|
-
qubx/utils/runner/factory.py,sha256=
|
|
159
|
+
qubx/utils/runner/factory.py,sha256=Mh_soHKci7Xj5uYI3vhxzXkrPUm2Z-Cq3PGC6w3KCUY,14620
|
|
159
160
|
qubx/utils/runner/runner.py,sha256=egv4OtPwEIwG0-Ia3ai9E9MYQIaslZLyoT4tL1FxQcY,28891
|
|
160
161
|
qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
|
|
161
162
|
qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
|
|
162
|
-
qubx-0.6.
|
|
163
|
-
qubx-0.6.
|
|
164
|
-
qubx-0.6.
|
|
165
|
-
qubx-0.6.
|
|
166
|
-
qubx-0.6.
|
|
163
|
+
qubx-0.6.41.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
|
|
164
|
+
qubx-0.6.41.dist-info/METADATA,sha256=TdDDd1n1SALzg1jwDaAG153aD7-MqDAJh40T--FQ-4c,4492
|
|
165
|
+
qubx-0.6.41.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
|
|
166
|
+
qubx-0.6.41.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
167
|
+
qubx-0.6.41.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|