Qubx 0.6.6__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.7__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/account.py +0 -1
- qubx/backtester/runner.py +8 -1
- qubx/connectors/ccxt/broker.py +10 -2
- qubx/core/context.py +16 -4
- qubx/core/exceptions.py +4 -0
- qubx/core/helpers.py +8 -2
- qubx/core/initializer.py +20 -1
- qubx/core/interfaces.py +25 -0
- qubx/core/lookups.py +1 -1
- qubx/core/mixins/processing.py +8 -1
- 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 +224 -134
- qubx/exporters/redis_streams.py +61 -21
- qubx/exporters/slack.py +37 -23
- qubx/notifications/slack.py +38 -20
- qubx/restorers/signal.py +3 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/trackers/riskctrl.py +3 -3
- qubx/utils/runner/_jupyter_runner.pyt +68 -7
- qubx/utils/runner/runner.py +17 -2
- {qubx-0.6.6.dist-info → qubx-0.6.7.dist-info}/METADATA +1 -1
- {qubx-0.6.6.dist-info → qubx-0.6.7.dist-info}/RECORD +25 -25
- {qubx-0.6.6.dist-info → qubx-0.6.7.dist-info}/WHEEL +0 -0
- {qubx-0.6.6.dist-info → qubx-0.6.7.dist-info}/entry_points.txt +0 -0
qubx/backtester/account.py
CHANGED
|
@@ -154,7 +154,6 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
154
154
|
return
|
|
155
155
|
for r in ome.process_market_data(data):
|
|
156
156
|
if r.exec is not None:
|
|
157
|
-
# TODO: why do we pop here if it is popped later after send
|
|
158
157
|
if r.order.id in self.order_to_instrument:
|
|
159
158
|
self.order_to_instrument.pop(r.order.id)
|
|
160
159
|
# - process methods will be called from stg context
|
qubx/backtester/runner.py
CHANGED
|
@@ -8,7 +8,8 @@ from qubx.core.basics import SW, DataType
|
|
|
8
8
|
from qubx.core.context import StrategyContext
|
|
9
9
|
from qubx.core.exceptions import SimulationConfigError, SimulationError
|
|
10
10
|
from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
|
|
11
|
-
from qubx.core.
|
|
11
|
+
from qubx.core.initializer import BasicStrategyInitializer
|
|
12
|
+
from qubx.core.interfaces import IMetricEmitter, IStrategy, IStrategyContext, StrategyState
|
|
12
13
|
from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
|
|
13
14
|
from qubx.core.lookups import lookup
|
|
14
15
|
from qubx.pandaz.utils import _frame_to_str
|
|
@@ -58,6 +59,8 @@ class SimulationRunner:
|
|
|
58
59
|
account_id: str = "SimulatedAccount",
|
|
59
60
|
portfolio_log_freq: str = "5Min",
|
|
60
61
|
emitter: IMetricEmitter | None = None,
|
|
62
|
+
strategy_state: StrategyState | None = None,
|
|
63
|
+
initializer: BasicStrategyInitializer | None = None,
|
|
61
64
|
):
|
|
62
65
|
"""
|
|
63
66
|
Initialize the BacktestContextRunner with a strategy context.
|
|
@@ -78,6 +81,8 @@ class SimulationRunner:
|
|
|
78
81
|
self.account_id = account_id
|
|
79
82
|
self.portfolio_log_freq = portfolio_log_freq
|
|
80
83
|
self.emitter = emitter
|
|
84
|
+
self.strategy_state = strategy_state if strategy_state is not None else StrategyState()
|
|
85
|
+
self.initializer = initializer
|
|
81
86
|
self.ctx = self._create_backtest_context()
|
|
82
87
|
|
|
83
88
|
# - get strategy parameters BEFORE simulation start
|
|
@@ -239,6 +244,8 @@ class SimulationRunner:
|
|
|
239
244
|
logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
|
|
240
245
|
aux_data_provider=_aux_data,
|
|
241
246
|
emitter=self.emitter,
|
|
247
|
+
strategy_state=self.strategy_state,
|
|
248
|
+
initializer=self.initializer,
|
|
242
249
|
)
|
|
243
250
|
|
|
244
251
|
# - setup base subscription from spec
|
qubx/connectors/ccxt/broker.py
CHANGED
|
@@ -11,6 +11,7 @@ from qubx.core.basics import (
|
|
|
11
11
|
Order,
|
|
12
12
|
Position,
|
|
13
13
|
)
|
|
14
|
+
from qubx.core.exceptions import InvalidOrderParameters
|
|
14
15
|
from qubx.core.interfaces import (
|
|
15
16
|
IAccountProcessor,
|
|
16
17
|
IBroker,
|
|
@@ -54,13 +55,20 @@ class CcxtBroker(IBroker):
|
|
|
54
55
|
price: float | None = None,
|
|
55
56
|
client_id: str | None = None,
|
|
56
57
|
time_in_force: str = "gtc",
|
|
58
|
+
**options,
|
|
57
59
|
) -> Order:
|
|
58
60
|
params = {}
|
|
61
|
+
_is_trigger_order = order_type.startswith("stop_")
|
|
59
62
|
|
|
60
|
-
if order_type == "limit":
|
|
63
|
+
if order_type == "limit" or _is_trigger_order:
|
|
61
64
|
params["timeInForce"] = time_in_force.upper()
|
|
62
65
|
if price is None:
|
|
63
|
-
raise
|
|
66
|
+
raise InvalidOrderParameters(f"Price must be specified for '{order_type}' order")
|
|
67
|
+
|
|
68
|
+
# - handle trigger (stop) orders
|
|
69
|
+
if _is_trigger_order:
|
|
70
|
+
params["triggerPrice"] = price
|
|
71
|
+
order_type = order_type.split("_")[1]
|
|
64
72
|
|
|
65
73
|
if client_id:
|
|
66
74
|
params["newClientOrderId"] = client_id
|
qubx/core/context.py
CHANGED
|
@@ -100,6 +100,7 @@ class StrategyContext(IStrategyContext):
|
|
|
100
100
|
lifecycle_notifier: IStrategyLifecycleNotifier | None = None,
|
|
101
101
|
initializer: BasicStrategyInitializer | None = None,
|
|
102
102
|
strategy_name: str | None = None,
|
|
103
|
+
strategy_state: StrategyState | None = None,
|
|
103
104
|
) -> None:
|
|
104
105
|
self.account = account
|
|
105
106
|
self.strategy = self.__instantiate_strategy(strategy, config)
|
|
@@ -122,7 +123,7 @@ class StrategyContext(IStrategyContext):
|
|
|
122
123
|
self._cache = CachedMarketDataHolder()
|
|
123
124
|
self._exporter = exporter
|
|
124
125
|
self._lifecycle_notifier = lifecycle_notifier
|
|
125
|
-
self._strategy_state = StrategyState()
|
|
126
|
+
self._strategy_state = strategy_state if strategy_state is not None else StrategyState()
|
|
126
127
|
self._strategy_name = strategy_name if strategy_name is not None else strategy.__class__.__name__
|
|
127
128
|
|
|
128
129
|
__position_tracker = self.strategy.tracker(self)
|
|
@@ -182,7 +183,9 @@ class StrategyContext(IStrategyContext):
|
|
|
182
183
|
self.__post_init__()
|
|
183
184
|
|
|
184
185
|
def __post_init__(self) -> None:
|
|
185
|
-
self.
|
|
186
|
+
if not self._strategy_state.is_on_init_called:
|
|
187
|
+
self.strategy.on_init(self.initializer)
|
|
188
|
+
self._strategy_state.is_on_init_called = True
|
|
186
189
|
|
|
187
190
|
if base_sub := self.initializer.get_base_subscription():
|
|
188
191
|
self.set_base_subscription(base_sub)
|
|
@@ -196,6 +199,14 @@ class StrategyContext(IStrategyContext):
|
|
|
196
199
|
if event_schedule := self.initializer.get_event_schedule():
|
|
197
200
|
self.set_event_schedule(event_schedule)
|
|
198
201
|
|
|
202
|
+
if pending_global_subscriptions := self.initializer.get_pending_global_subscriptions():
|
|
203
|
+
for sub_type in pending_global_subscriptions:
|
|
204
|
+
self.subscribe(sub_type)
|
|
205
|
+
|
|
206
|
+
if pending_instrument_subscriptions := self.initializer.get_pending_instrument_subscriptions():
|
|
207
|
+
for sub_type, instruments in pending_instrument_subscriptions.items():
|
|
208
|
+
self.subscribe(sub_type, list(instruments))
|
|
209
|
+
|
|
199
210
|
# - update cache default timeframe
|
|
200
211
|
sub_type = self.get_base_subscription()
|
|
201
212
|
_, params = DataType.from_str(sub_type)
|
|
@@ -208,7 +219,7 @@ class StrategyContext(IStrategyContext):
|
|
|
208
219
|
|
|
209
220
|
# Update initial instruments if strategy set them after warmup
|
|
210
221
|
if self.get_warmup_positions():
|
|
211
|
-
self._initial_instruments = list(self.get_warmup_positions().keys())
|
|
222
|
+
self._initial_instruments = list(set(self.get_warmup_positions().keys()) | set(self._initial_instruments))
|
|
212
223
|
|
|
213
224
|
# Notify strategy start
|
|
214
225
|
if self._lifecycle_notifier:
|
|
@@ -263,7 +274,8 @@ class StrategyContext(IStrategyContext):
|
|
|
263
274
|
|
|
264
275
|
# - invoke strategy's stop code
|
|
265
276
|
try:
|
|
266
|
-
self.
|
|
277
|
+
if not self._strategy_state.is_warmup_in_progress:
|
|
278
|
+
self.strategy.on_stop(self)
|
|
267
279
|
except Exception as strat_error:
|
|
268
280
|
logger.error(
|
|
269
281
|
f"[<y>StrategyContext</y>] :: Strategy {self._strategy_name} raised an exception in on_stop: {strat_error}"
|
qubx/core/exceptions.py
CHANGED
qubx/core/helpers.py
CHANGED
|
@@ -109,13 +109,19 @@ class CachedMarketDataHolder:
|
|
|
109
109
|
return list(self._instr_to_sub_to_buffer[instrument][event_type])
|
|
110
110
|
|
|
111
111
|
def update(
|
|
112
|
-
self,
|
|
112
|
+
self,
|
|
113
|
+
instrument: Instrument,
|
|
114
|
+
event_type: str,
|
|
115
|
+
data: Any,
|
|
116
|
+
update_ohlc: bool = False,
|
|
117
|
+
is_historical: bool = False,
|
|
118
|
+
is_base_data: bool = True,
|
|
113
119
|
) -> None:
|
|
114
120
|
# - store data in buffer if it's not OHLC
|
|
115
121
|
if event_type != DataType.OHLC:
|
|
116
122
|
self._instr_to_sub_to_buffer[instrument][event_type].append(data)
|
|
117
123
|
|
|
118
|
-
if not is_historical:
|
|
124
|
+
if not is_historical and is_base_data:
|
|
119
125
|
self._ready_instruments.add(instrument)
|
|
120
126
|
|
|
121
127
|
if not update_ohlc:
|
qubx/core/initializer.py
CHANGED
|
@@ -8,7 +8,7 @@ schedules, warmup periods, and position mismatch resolution.
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any, Dict, Optional
|
|
10
10
|
|
|
11
|
-
from qubx.core.basics import td_64
|
|
11
|
+
from qubx.core.basics import Instrument, td_64
|
|
12
12
|
from qubx.core.interfaces import IStrategyInitializer, StartTimeFinderProtocol, StateResolverProtocol
|
|
13
13
|
from qubx.core.utils import recognize_timeframe
|
|
14
14
|
|
|
@@ -35,6 +35,9 @@ class BasicStrategyInitializer(IStrategyInitializer):
|
|
|
35
35
|
# Additional configuration that might be needed
|
|
36
36
|
config: Dict[str, Any] = field(default_factory=dict)
|
|
37
37
|
|
|
38
|
+
_pending_global_subscriptions: set[str] = field(default_factory=set)
|
|
39
|
+
_pending_instrument_subscriptions: dict[str, set[Instrument]] = field(default_factory=dict)
|
|
40
|
+
|
|
38
41
|
def set_base_subscription(self, subscription_type: str) -> None:
|
|
39
42
|
self.base_subscription = subscription_type
|
|
40
43
|
|
|
@@ -87,3 +90,19 @@ class BasicStrategyInitializer(IStrategyInitializer):
|
|
|
87
90
|
@property
|
|
88
91
|
def is_simulation(self) -> bool | None:
|
|
89
92
|
return self.simulation
|
|
93
|
+
|
|
94
|
+
def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
|
|
95
|
+
if instruments is None:
|
|
96
|
+
self._pending_global_subscriptions.add(subscription_type)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
if isinstance(instruments, Instrument):
|
|
100
|
+
instruments = [instruments]
|
|
101
|
+
|
|
102
|
+
self._pending_instrument_subscriptions[subscription_type].update(instruments)
|
|
103
|
+
|
|
104
|
+
def get_pending_global_subscriptions(self) -> set[str]:
|
|
105
|
+
return self._pending_global_subscriptions
|
|
106
|
+
|
|
107
|
+
def get_pending_instrument_subscriptions(self) -> dict[str, set[Instrument]]:
|
|
108
|
+
return self._pending_instrument_subscriptions
|
qubx/core/interfaces.py
CHANGED
|
@@ -978,14 +978,18 @@ class IWarmupStateSaver:
|
|
|
978
978
|
|
|
979
979
|
@dataclass
|
|
980
980
|
class StrategyState:
|
|
981
|
+
is_on_init_called: bool = False
|
|
981
982
|
is_on_start_called: bool = False
|
|
982
983
|
is_on_warmup_finished_called: bool = False
|
|
983
984
|
is_on_fit_called: bool = False
|
|
985
|
+
is_warmup_in_progress: bool = False
|
|
984
986
|
|
|
985
987
|
def reset_from_state(self, state: "StrategyState"):
|
|
988
|
+
self.is_on_init_called = state.is_on_init_called
|
|
986
989
|
self.is_on_start_called = state.is_on_start_called
|
|
987
990
|
self.is_on_warmup_finished_called = state.is_on_warmup_finished_called
|
|
988
991
|
self.is_on_fit_called = state.is_on_fit_called
|
|
992
|
+
self.is_warmup_in_progress = state.is_warmup_in_progress
|
|
989
993
|
|
|
990
994
|
|
|
991
995
|
class IStrategyContext(
|
|
@@ -1380,6 +1384,27 @@ class IStrategyInitializer:
|
|
|
1380
1384
|
"""
|
|
1381
1385
|
...
|
|
1382
1386
|
|
|
1387
|
+
def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
|
|
1388
|
+
"""Subscribe to market data for an instrument.
|
|
1389
|
+
|
|
1390
|
+
Args:
|
|
1391
|
+
subscription_type: Type of subscription. If None, the base subscription type is used.
|
|
1392
|
+
instruments: A list of instrument of instrument to subscribe to
|
|
1393
|
+
"""
|
|
1394
|
+
...
|
|
1395
|
+
|
|
1396
|
+
def get_pending_global_subscriptions(self) -> set[str]:
|
|
1397
|
+
"""
|
|
1398
|
+
Get the pending global subscriptions.
|
|
1399
|
+
"""
|
|
1400
|
+
...
|
|
1401
|
+
|
|
1402
|
+
def get_pending_instrument_subscriptions(self) -> dict[str, set[Instrument]]:
|
|
1403
|
+
"""
|
|
1404
|
+
Get the pending instrument subscriptions.
|
|
1405
|
+
"""
|
|
1406
|
+
...
|
|
1407
|
+
|
|
1383
1408
|
|
|
1384
1409
|
class IStrategy(metaclass=Mixable):
|
|
1385
1410
|
"""Base class for trading strategies."""
|
qubx/core/lookups.py
CHANGED
|
@@ -380,7 +380,7 @@ class FeesLookup:
|
|
|
380
380
|
|
|
381
381
|
# - check if spec is of type maker=...,taker=...
|
|
382
382
|
# Check if spec is in the format maker=X,taker=Y
|
|
383
|
-
maker_taker_pattern = re.compile(r"maker=(-?[0-9.]+)
|
|
383
|
+
maker_taker_pattern = re.compile(r"maker=(-?[0-9.]+)[,\ ]taker=(-?[0-9.]+)")
|
|
384
384
|
match = maker_taker_pattern.match(spec)
|
|
385
385
|
if match:
|
|
386
386
|
maker_rate, taker_rate = float(match.group(1)), float(match.group(2))
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -346,7 +346,14 @@ class ProcessingManager(IProcessingManager):
|
|
|
346
346
|
|
|
347
347
|
# update cached ohlc is this is base subscription
|
|
348
348
|
_update_ohlc = is_base_data
|
|
349
|
-
self._cache.update(
|
|
349
|
+
self._cache.update(
|
|
350
|
+
instrument,
|
|
351
|
+
event_type,
|
|
352
|
+
_update,
|
|
353
|
+
update_ohlc=_update_ohlc,
|
|
354
|
+
is_historical=is_historical,
|
|
355
|
+
is_base_data=is_base_data,
|
|
356
|
+
)
|
|
350
357
|
|
|
351
358
|
# update trackers, gatherers on base data
|
|
352
359
|
if not is_historical:
|
|
Binary file
|
|
Binary file
|