Qubx 0.6.3__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.6__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 +3 -1
- qubx/backtester/ome.py +56 -41
- qubx/backtester/runner.py +15 -5
- qubx/backtester/simulated_data.py +2 -156
- qubx/backtester/utils.py +15 -3
- qubx/cli/commands.py +10 -35
- qubx/cli/deploy.py +3 -5
- qubx/cli/misc.py +54 -20
- qubx/cli/release.py +219 -105
- qubx/connectors/ccxt/__init__.py +3 -0
- qubx/connectors/ccxt/data.py +1 -1
- qubx/connectors/ccxt/reader.py +237 -0
- qubx/connectors/ccxt/utils.py +48 -37
- qubx/core/account.py +2 -1
- qubx/core/basics.py +4 -2
- qubx/core/context.py +95 -22
- qubx/core/helpers.py +60 -14
- qubx/core/initializer.py +14 -9
- qubx/core/interfaces.py +187 -25
- qubx/core/lookups.py +26 -32
- qubx/core/metrics.py +5 -9
- qubx/core/mixins/market.py +2 -4
- qubx/core/mixins/processing.py +118 -17
- qubx/core/mixins/trading.py +12 -3
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +2 -0
- qubx/core/series.pyi +1 -0
- qubx/core/series.pyx +136 -2
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/data/__init__.py +2 -0
- qubx/data/composite.py +417 -75
- qubx/data/helpers.py +18 -27
- qubx/data/hft.py +51 -3
- qubx/data/readers.py +32 -17
- qubx/data/registry.py +124 -0
- qubx/data/tardis.py +693 -4
- qubx/emitters/__init__.py +17 -0
- qubx/emitters/base.py +206 -0
- qubx/emitters/composite.py +78 -0
- qubx/emitters/prometheus.py +222 -0
- qubx/emitters/questdb.py +126 -0
- qubx/gathering/simplest.py +1 -1
- qubx/notifications/__init__.py +11 -0
- qubx/notifications/composite.py +71 -0
- qubx/notifications/slack.py +156 -0
- qubx/pandaz/ta.py +3 -1
- qubx/resources/_build.py +237 -0
- qubx/restarts/state_resolvers.py +68 -16
- qubx/restarts/time_finders.py +31 -1
- qubx/restorers/interfaces.py +2 -2
- qubx/restorers/signal.py +29 -20
- qubx/restorers/state.py +9 -7
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/trackers/riskctrl.py +31 -8
- qubx/utils/misc.py +43 -0
- qubx/utils/orderbook.py +43 -1
- qubx/utils/runner/_jupyter_runner.pyt +62 -16
- qubx/utils/runner/configs.py +37 -5
- qubx/utils/runner/runner.py +392 -67
- qubx/utils/time.py +14 -0
- qubx/utils/version.py +1 -1
- {qubx-0.6.3.dist-info → qubx-0.6.6.dist-info}/METADATA +4 -1
- {qubx-0.6.3.dist-info → qubx-0.6.6.dist-info}/RECORD +65 -54
- {qubx-0.6.3.dist-info → qubx-0.6.6.dist-info}/WHEEL +0 -0
- {qubx-0.6.3.dist-info → qubx-0.6.6.dist-info}/entry_points.txt +0 -0
qubx/backtester/account.py
CHANGED
|
@@ -154,7 +154,9 @@ 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
|
-
|
|
157
|
+
# TODO: why do we pop here if it is popped later after send
|
|
158
|
+
if r.order.id in self.order_to_instrument:
|
|
159
|
+
self.order_to_instrument.pop(r.order.id)
|
|
158
160
|
# - process methods will be called from stg context
|
|
159
161
|
self._channel.send((instrument, "order", r.order, False))
|
|
160
162
|
self._channel.send((instrument, "deals", [r.exec], False))
|
qubx/backtester/ome.py
CHANGED
|
@@ -7,6 +7,8 @@ from sortedcontainers import SortedDict
|
|
|
7
7
|
from qubx import logger
|
|
8
8
|
from qubx.core.basics import (
|
|
9
9
|
OPTION_FILL_AT_SIGNAL_PRICE,
|
|
10
|
+
OPTION_SIGNAL_PRICE,
|
|
11
|
+
OPTION_SKIP_PRICE_CROSS_CONTROL,
|
|
10
12
|
Deal,
|
|
11
13
|
Instrument,
|
|
12
14
|
ITimeProvider,
|
|
@@ -194,54 +196,67 @@ class OrdersManagementEngine:
|
|
|
194
196
|
if order.status in ["CLOSED", "CANCELED"]:
|
|
195
197
|
raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
|
|
196
198
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
199
|
+
_buy_side = order.side == "BUY"
|
|
200
|
+
_c_ask = self.bbo.ask # type: ignore
|
|
201
|
+
_c_bid = self.bbo.bid # type: ignore
|
|
200
202
|
|
|
201
203
|
# - check if order can be "executed" immediately
|
|
202
|
-
|
|
204
|
+
_exec_price = None
|
|
203
205
|
_need_update_book = False
|
|
204
206
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
207
|
+
match order.type:
|
|
208
|
+
case "MARKET":
|
|
209
|
+
if _exec_price is None:
|
|
210
|
+
_exec_price = _c_ask if _buy_side else _c_bid
|
|
211
|
+
|
|
212
|
+
# - special case only for simulation: exact fill at signal price for market orders
|
|
213
|
+
_fill_at_signal_price = order.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
|
|
214
|
+
_signal_price = order.options.get(OPTION_SIGNAL_PRICE, None)
|
|
215
|
+
|
|
216
|
+
# - some cases require to skip price cross control
|
|
217
|
+
_skip_price_cross_control = order.options.get(OPTION_SKIP_PRICE_CROSS_CONTROL, False)
|
|
218
|
+
|
|
219
|
+
# - it's passed only if signal price is valid: market crossed this desired price on last update
|
|
220
|
+
if _fill_at_signal_price and _signal_price and self.__prev_bbo:
|
|
221
|
+
_desired_fill_price = _signal_price
|
|
222
|
+
_prev_mp = self.__prev_bbo.mid_price()
|
|
223
|
+
_c_mid_price = self.bbo.mid_price() # type: ignore
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
_skip_price_cross_control
|
|
227
|
+
or (_prev_mp < _desired_fill_price <= _c_mid_price)
|
|
228
|
+
or (_prev_mp > _desired_fill_price >= _c_mid_price)
|
|
229
|
+
):
|
|
230
|
+
_exec_price = _desired_fill_price
|
|
231
|
+
else:
|
|
232
|
+
raise SimulationError(
|
|
233
|
+
f"Special execution price at {_desired_fill_price} for market order {order.id} cannot be filled because market didn't cross this price on last update !"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
case "LIMIT":
|
|
237
|
+
_need_update_book = True
|
|
238
|
+
if (_buy_side and order.price >= _c_ask) or (not _buy_side and order.price <= _c_bid):
|
|
239
|
+
_exec_price = _c_ask if _buy_side else _c_bid
|
|
240
|
+
|
|
241
|
+
case "STOP_MARKET":
|
|
242
|
+
# - it processes stop orders separately without adding to orderbook (as on real exchanges)
|
|
243
|
+
order.status = "OPEN"
|
|
244
|
+
self.stop_orders[order.id] = order
|
|
245
|
+
|
|
246
|
+
case "STOP_LIMIT":
|
|
247
|
+
# TODO: (OME) check trigger conditions in options etc
|
|
248
|
+
raise NotImplementedError("'STOP_LIMIT' order is not supported in Qubx simulator yet !")
|
|
249
|
+
|
|
250
|
+
case _:
|
|
251
|
+
raise SimulationError(f"Invalid order type: {order.type} for {self.instrument.symbol}")
|
|
237
252
|
|
|
238
253
|
# - if order must be "executed" immediately
|
|
239
|
-
if
|
|
240
|
-
return self._execute_order(timestamp,
|
|
254
|
+
if _exec_price is not None:
|
|
255
|
+
return self._execute_order(timestamp, _exec_price, order, True)
|
|
241
256
|
|
|
242
257
|
# - processing limit orders
|
|
243
258
|
if _need_update_book:
|
|
244
|
-
if
|
|
259
|
+
if _buy_side:
|
|
245
260
|
self.bids.setdefault(order.price, list()).append(order.id)
|
|
246
261
|
else:
|
|
247
262
|
self.asks.setdefault(order.price, list()).append(order.id)
|
|
@@ -288,8 +303,8 @@ class OrdersManagementEngine:
|
|
|
288
303
|
if (_ot == "LIMIT" or _ot.startswith("STOP")) and (price is None or price <= 0):
|
|
289
304
|
raise InvalidOrder("Invalid order price. Price must be positively defined for LIMIT or STOP orders.")
|
|
290
305
|
|
|
291
|
-
if time_in_force.upper() not in ["GTC", "IOC"]:
|
|
292
|
-
raise InvalidOrder("Invalid time in force. Only GTC
|
|
306
|
+
if time_in_force.upper() not in ["GTC", "IOC", "GTX"]:
|
|
307
|
+
raise InvalidOrder("Invalid time in force. Only GTC, IOC, GTX are supported for now.")
|
|
293
308
|
|
|
294
309
|
if _ot.startswith("STOP"):
|
|
295
310
|
assert price is not None
|
qubx/backtester/runner.py
CHANGED
|
@@ -8,7 +8,7 @@ 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.interfaces import IStrategy, IStrategyContext
|
|
11
|
+
from qubx.core.interfaces import IMetricEmitter, IStrategy, IStrategyContext
|
|
12
12
|
from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
|
|
13
13
|
from qubx.core.lookups import lookup
|
|
14
14
|
from qubx.pandaz.utils import _frame_to_str
|
|
@@ -57,6 +57,7 @@ class SimulationRunner:
|
|
|
57
57
|
stop: pd.Timestamp | str,
|
|
58
58
|
account_id: str = "SimulatedAccount",
|
|
59
59
|
portfolio_log_freq: str = "5Min",
|
|
60
|
+
emitter: IMetricEmitter | None = None,
|
|
60
61
|
):
|
|
61
62
|
"""
|
|
62
63
|
Initialize the BacktestContextRunner with a strategy context.
|
|
@@ -68,6 +69,7 @@ class SimulationRunner:
|
|
|
68
69
|
stop (pd.Timestamp): The end time of the simulation.
|
|
69
70
|
account_id (str): The account id to use.
|
|
70
71
|
portfolio_log_freq (str): The portfolio log frequency to use.
|
|
72
|
+
emitter (IMetricEmitter): The emitter to use.
|
|
71
73
|
"""
|
|
72
74
|
self.setup = setup
|
|
73
75
|
self.data_config = data_config
|
|
@@ -75,6 +77,7 @@ class SimulationRunner:
|
|
|
75
77
|
self.stop = pd.Timestamp(stop)
|
|
76
78
|
self.account_id = account_id
|
|
77
79
|
self.portfolio_log_freq = portfolio_log_freq
|
|
80
|
+
self.emitter = emitter
|
|
78
81
|
self.ctx = self._create_backtest_context()
|
|
79
82
|
|
|
80
83
|
# - get strategy parameters BEFORE simulation start
|
|
@@ -85,7 +88,7 @@ class SimulationRunner:
|
|
|
85
88
|
self.strategy_params = extract_parameters_from_object(self.setup.generator)
|
|
86
89
|
self.strategy_class = full_qualified_class_name(self.setup.generator)
|
|
87
90
|
|
|
88
|
-
def run(self, silent: bool = False):
|
|
91
|
+
def run(self, silent: bool = False, catch_keyboard_interrupt: bool = True, close_data_readers: bool = False):
|
|
89
92
|
"""
|
|
90
93
|
Run the backtest from start to stop.
|
|
91
94
|
|
|
@@ -94,7 +97,7 @@ class SimulationRunner:
|
|
|
94
97
|
stop (pd.Timestamp | str): The end time of the simulation.
|
|
95
98
|
silent (bool, optional): Whether to suppress progress output. Defaults to False.
|
|
96
99
|
"""
|
|
97
|
-
logger.debug(f"[<y>
|
|
100
|
+
logger.debug(f"[<y>SimulationRunner</y>] :: Running simulation from {self.start} to {self.stop}")
|
|
98
101
|
|
|
99
102
|
# Start the context
|
|
100
103
|
self.ctx.start()
|
|
@@ -103,7 +106,7 @@ class SimulationRunner:
|
|
|
103
106
|
for s in self.ctx.get_subscriptions():
|
|
104
107
|
if not self.ctx.get_warmup(s) and (_d_wt := self.data_config.default_warmups.get(s)):
|
|
105
108
|
logger.debug(
|
|
106
|
-
f"[<y>
|
|
109
|
+
f"[<y>SimulationRunner</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
|
|
107
110
|
)
|
|
108
111
|
self.ctx.set_warmup({s: _d_wt})
|
|
109
112
|
|
|
@@ -129,13 +132,19 @@ class SimulationRunner:
|
|
|
129
132
|
stop = self._stop or self.stop
|
|
130
133
|
|
|
131
134
|
try:
|
|
132
|
-
# Run the data provider
|
|
133
135
|
self.data_provider.run(self.start, stop, silent=silent)
|
|
134
136
|
except KeyboardInterrupt:
|
|
135
137
|
logger.error("Simulated trading interrupted by user!")
|
|
138
|
+
if not catch_keyboard_interrupt:
|
|
139
|
+
raise
|
|
136
140
|
finally:
|
|
137
141
|
# Stop the context
|
|
138
142
|
self.ctx.stop()
|
|
143
|
+
if close_data_readers:
|
|
144
|
+
assert isinstance(self.data_provider, SimulatedDataProvider)
|
|
145
|
+
for reader in self.data_provider._readers.values():
|
|
146
|
+
if hasattr(reader, "close"):
|
|
147
|
+
reader.close() # type: ignore
|
|
139
148
|
|
|
140
149
|
def print_latency_report(self) -> None:
|
|
141
150
|
_l_r = SW.latency_report()
|
|
@@ -229,6 +238,7 @@ class SimulationRunner:
|
|
|
229
238
|
instruments=self.setup.instruments,
|
|
230
239
|
logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
|
|
231
240
|
aux_data_provider=_aux_data,
|
|
241
|
+
emitter=self.emitter,
|
|
232
242
|
)
|
|
233
243
|
|
|
234
244
|
# - setup base subscription from spec
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
from collections import defaultdict, deque
|
|
3
|
-
from typing import Any, Iterator, TypeAlias
|
|
1
|
+
from typing import Any, Iterator
|
|
4
2
|
|
|
5
3
|
import pandas as pd
|
|
6
4
|
|
|
7
5
|
from qubx import logger
|
|
8
6
|
from qubx.core.basics import DataType, Instrument, Timestamped
|
|
9
7
|
from qubx.core.exceptions import SimulationError
|
|
8
|
+
from qubx.data.composite import IteratedDataStreamsSlicer
|
|
10
9
|
from qubx.data.readers import (
|
|
11
10
|
AsDict,
|
|
12
11
|
AsOrderBook,
|
|
@@ -19,159 +18,6 @@ from qubx.data.readers import (
|
|
|
19
18
|
RestoreTradesFromOHLC,
|
|
20
19
|
)
|
|
21
20
|
|
|
22
|
-
SlicerOutData: TypeAlias = tuple[str, int, Timestamped] | tuple
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
|
|
26
|
-
"""
|
|
27
|
-
This class manages seamless iteration over multiple time-series data streams,
|
|
28
|
-
ensuring that events are processed in the correct chronological order regardless of their source.
|
|
29
|
-
It supports adding / removing new data streams to the slicer on the fly (during the itration).
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
_iterators: dict[str, Iterator[list[Timestamped]]]
|
|
33
|
-
_buffers: dict[str, list[Timestamped]]
|
|
34
|
-
_keys: deque[str]
|
|
35
|
-
_iterating: bool
|
|
36
|
-
|
|
37
|
-
def __init__(self):
|
|
38
|
-
self._buffers = defaultdict(list)
|
|
39
|
-
self._iterators = {}
|
|
40
|
-
self._keys = deque()
|
|
41
|
-
self._iterating = False
|
|
42
|
-
|
|
43
|
-
def put(self, data: dict[str, Iterator[list[Timestamped]]]):
|
|
44
|
-
_rebuild = False
|
|
45
|
-
for k, vi in data.items():
|
|
46
|
-
if k not in self._keys:
|
|
47
|
-
self._iterators[k] = vi
|
|
48
|
-
self._buffers[k] = self._load_next_chunk_to_buffer(k) # do initial chunk fetching
|
|
49
|
-
self._keys.append(k)
|
|
50
|
-
_rebuild = True
|
|
51
|
-
|
|
52
|
-
# - rebuild strategy
|
|
53
|
-
if _rebuild and self._iterating:
|
|
54
|
-
self._build_initial_iteration_seq()
|
|
55
|
-
|
|
56
|
-
def __add__(self, data: dict[str, Iterator]) -> "IteratedDataStreamsSlicer":
|
|
57
|
-
self.put(data)
|
|
58
|
-
return self
|
|
59
|
-
|
|
60
|
-
def remove(self, keys: list[str] | str):
|
|
61
|
-
"""
|
|
62
|
-
Remove data iterator and associated keys from the queue.
|
|
63
|
-
If the key is not found, it does nothing.
|
|
64
|
-
"""
|
|
65
|
-
_keys = keys if isinstance(keys, list) else [keys]
|
|
66
|
-
_rebuild = False
|
|
67
|
-
for i in _keys:
|
|
68
|
-
if i in self._buffers:
|
|
69
|
-
self._buffers.pop(i)
|
|
70
|
-
self._iterators.pop(i)
|
|
71
|
-
self._keys.remove(i)
|
|
72
|
-
_rebuild = True
|
|
73
|
-
|
|
74
|
-
# - rebuild strategy
|
|
75
|
-
if _rebuild and self._iterating:
|
|
76
|
-
self._build_initial_iteration_seq()
|
|
77
|
-
|
|
78
|
-
def __iter__(self) -> Iterator:
|
|
79
|
-
self._build_initial_iteration_seq()
|
|
80
|
-
self._iterating = True
|
|
81
|
-
return self
|
|
82
|
-
|
|
83
|
-
def _build_initial_iteration_seq(self):
|
|
84
|
-
_init_seq = {k: self._buffers[k][-1].time for k in self._keys}
|
|
85
|
-
_init_seq = dict(sorted(_init_seq.items(), key=lambda item: item[1]))
|
|
86
|
-
self._keys = deque(_init_seq.keys())
|
|
87
|
-
|
|
88
|
-
def _load_next_chunk_to_buffer(self, index: str) -> list[Timestamped]:
|
|
89
|
-
return list(reversed(next(self._iterators[index])))
|
|
90
|
-
|
|
91
|
-
def _remove_iterator(self, key: str):
|
|
92
|
-
self._buffers.pop(key)
|
|
93
|
-
self._iterators.pop(key)
|
|
94
|
-
self._keys.remove(key)
|
|
95
|
-
|
|
96
|
-
def _pop_top(self, k: str) -> Timestamped:
|
|
97
|
-
"""
|
|
98
|
-
Removes and returns the most recent timestamped data element from the buffer associated with the given key.
|
|
99
|
-
If the buffer is empty after popping, it attempts to load the next chunk of data into the buffer.
|
|
100
|
-
If no more data is available, the iterator associated with the key is removed.
|
|
101
|
-
|
|
102
|
-
Parameters:
|
|
103
|
-
k (str): The key identifying the data stream buffer to pop from.
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
Timestamped: The most recent timestamped data element from the buffer.
|
|
107
|
-
"""
|
|
108
|
-
v = (data := self._buffers[k]).pop()
|
|
109
|
-
if not data:
|
|
110
|
-
try:
|
|
111
|
-
data.extend(self._load_next_chunk_to_buffer(k)) # - get next chunk of data
|
|
112
|
-
except StopIteration:
|
|
113
|
-
self._remove_iterator(k) # - remove iterable data
|
|
114
|
-
return v
|
|
115
|
-
|
|
116
|
-
def fetch_before_time(self, key: str, time_ns: int) -> list[Timestamped]:
|
|
117
|
-
"""
|
|
118
|
-
Fetches and returns all timestamped data elements from the buffer associated with the given key
|
|
119
|
-
that have a timestamp earlier than the specified time.
|
|
120
|
-
|
|
121
|
-
Parameters:
|
|
122
|
-
- key (str): The key identifying the data stream buffer to fetch from.
|
|
123
|
-
- time_ns (int): The timestamp in nanoseconds. All returned elements will have a timestamp less than this value.
|
|
124
|
-
|
|
125
|
-
Returns:
|
|
126
|
-
- list[Timestamped]: A list of timestamped data elements that occur before the specified time.
|
|
127
|
-
"""
|
|
128
|
-
values = []
|
|
129
|
-
data = self._buffers[key]
|
|
130
|
-
if not data:
|
|
131
|
-
try:
|
|
132
|
-
data.extend(self._load_next_chunk_to_buffer(key)) # - get next chunk of data
|
|
133
|
-
except StopIteration:
|
|
134
|
-
self._remove_iterator(key)
|
|
135
|
-
|
|
136
|
-
# pull most past elements
|
|
137
|
-
v = data[-1]
|
|
138
|
-
while v.time < time_ns:
|
|
139
|
-
values.append(data.pop())
|
|
140
|
-
if not data:
|
|
141
|
-
try:
|
|
142
|
-
data.extend(self._load_next_chunk_to_buffer(key)) # - get next chunk of data
|
|
143
|
-
except StopIteration:
|
|
144
|
-
self._remove_iterator(key)
|
|
145
|
-
break
|
|
146
|
-
v = data[-1]
|
|
147
|
-
|
|
148
|
-
return values
|
|
149
|
-
|
|
150
|
-
def __next__(self) -> SlicerOutData:
|
|
151
|
-
"""
|
|
152
|
-
Advances the iterator to the next available timestamped data element across all data streams.
|
|
153
|
-
|
|
154
|
-
Returns:
|
|
155
|
-
- SlicerOutData: A tuple containing the key of the data stream, the timestamp of the data element, and the data element itself.
|
|
156
|
-
|
|
157
|
-
Raises:
|
|
158
|
-
- StopIteration: If there are no more data elements to iterate over.
|
|
159
|
-
"""
|
|
160
|
-
if not self._keys:
|
|
161
|
-
self._iterating = False
|
|
162
|
-
raise StopIteration
|
|
163
|
-
|
|
164
|
-
_min_t = math.inf
|
|
165
|
-
_min_k = self._keys[0]
|
|
166
|
-
for i in self._keys:
|
|
167
|
-
_x = self._buffers[i][-1]
|
|
168
|
-
if _x.time < _min_t:
|
|
169
|
-
_min_t = _x.time
|
|
170
|
-
_min_k = i
|
|
171
|
-
|
|
172
|
-
_v = self._pop_top(_min_k)
|
|
173
|
-
return (_min_k, _v.time, _v)
|
|
174
|
-
|
|
175
21
|
|
|
176
22
|
class DataFetcher:
|
|
177
23
|
_fetcher_id: str
|
qubx/backtester/utils.py
CHANGED
|
@@ -23,6 +23,7 @@ from qubx.core.interfaces import IStrategy, IStrategyContext, PositionsTracker
|
|
|
23
23
|
from qubx.core.lookups import lookup
|
|
24
24
|
from qubx.core.series import OHLCV, Bar, Quote, Trade
|
|
25
25
|
from qubx.core.utils import time_delta_to_str
|
|
26
|
+
from qubx.data import TardisMachineReader
|
|
26
27
|
from qubx.data.helpers import InMemoryCachedReader, TimeGuardedWrapper
|
|
27
28
|
from qubx.data.hft import HftDataReader
|
|
28
29
|
from qubx.data.readers import AsDict, DataReader, InMemoryDataFrameReader
|
|
@@ -86,9 +87,9 @@ class SimulationSetup:
|
|
|
86
87
|
exchange: str
|
|
87
88
|
capital: float
|
|
88
89
|
base_currency: str
|
|
89
|
-
commissions: str | None
|
|
90
|
-
signal_timeframe: str
|
|
91
|
-
accurate_stop_orders_execution: bool
|
|
90
|
+
commissions: str | None = None
|
|
91
|
+
signal_timeframe: str = "1Min"
|
|
92
|
+
accurate_stop_orders_execution: bool = False
|
|
92
93
|
|
|
93
94
|
def __str__(self) -> str:
|
|
94
95
|
return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchange}[{self.commissions}]"
|
|
@@ -729,6 +730,12 @@ def recognize_simulation_data_config(
|
|
|
729
730
|
|
|
730
731
|
_available_symbols = list(set.intersection(*_sets_of_symbols.values()))
|
|
731
732
|
|
|
733
|
+
case TardisMachineReader():
|
|
734
|
+
_supported_types = [DataType.ORDERBOOK, DataType.TRADE]
|
|
735
|
+
_available_symbols = decls.get_symbols(exchange, _supported_types[0])
|
|
736
|
+
for _type in _supported_types:
|
|
737
|
+
_requests[_type] = (_type, decls)
|
|
738
|
+
|
|
732
739
|
case DataReader():
|
|
733
740
|
_supported_data_type = sniffer._sniff_reader(f"{exchange}:{instruments[0].symbol}", decls, None)
|
|
734
741
|
_available_symbols = decls.get_symbols(exchange, DataType.from_str(_supported_data_type)[0])
|
|
@@ -766,6 +773,11 @@ def recognize_simulation_data_config(
|
|
|
766
773
|
_available_symbols = _provider.get_symbols(exchange, _supported_data_type)
|
|
767
774
|
_requests[_requested_type] = (_supported_data_type, _provider)
|
|
768
775
|
|
|
776
|
+
case TardisMachineReader():
|
|
777
|
+
_supported_data_type = _requested_type
|
|
778
|
+
_available_symbols = _provider.get_symbols(exchange, _supported_data_type)
|
|
779
|
+
_requests[_requested_type] = (_supported_data_type, _provider)
|
|
780
|
+
|
|
769
781
|
case DataReader():
|
|
770
782
|
_supported_data_type = sniffer._sniff_reader(
|
|
771
783
|
f"{exchange}:{instruments[0].symbol}", _provider, _requested_type
|
qubx/cli/commands.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import sys
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
5
4
|
import click
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
6
|
|
|
7
7
|
from qubx import QubxLogConfig, logger
|
|
8
8
|
|
|
@@ -32,13 +32,18 @@ def main(debug: bool, debug_port: int, log_level: str):
|
|
|
32
32
|
"""
|
|
33
33
|
Qubx CLI.
|
|
34
34
|
"""
|
|
35
|
+
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
|
|
35
36
|
log_level = log_level.upper() if not debug else "DEBUG"
|
|
36
37
|
|
|
38
|
+
env_file = Path.cwd().joinpath(".env")
|
|
39
|
+
if env_file.exists():
|
|
40
|
+
logger.info(f"Loading environment variables from {env_file}")
|
|
41
|
+
load_dotenv(env_file)
|
|
42
|
+
log_level = os.getenv("QUBX_LOG_LEVEL", log_level)
|
|
43
|
+
|
|
37
44
|
QubxLogConfig.set_log_level(log_level)
|
|
38
45
|
|
|
39
46
|
if debug:
|
|
40
|
-
os.environ["PYDEVD_DISABLE_FILE_VALIDATION"] = "1"
|
|
41
|
-
|
|
42
47
|
import debugpy
|
|
43
48
|
|
|
44
49
|
logger.info(f"Waiting for debugger to attach (port {debug_port})")
|
|
@@ -146,7 +151,7 @@ def ls(directory: str):
|
|
|
146
151
|
"-o",
|
|
147
152
|
type=click.STRING,
|
|
148
153
|
help="Output directory to put zip file.",
|
|
149
|
-
default="releases",
|
|
154
|
+
default=".releases",
|
|
150
155
|
show_default=True,
|
|
151
156
|
)
|
|
152
157
|
@click.option(
|
|
@@ -173,27 +178,6 @@ def ls(directory: str):
|
|
|
173
178
|
help="Commit changes and create tag in repo (default: False)",
|
|
174
179
|
show_default=True,
|
|
175
180
|
)
|
|
176
|
-
@click.option(
|
|
177
|
-
"--default-exchange",
|
|
178
|
-
type=click.STRING,
|
|
179
|
-
help="Default exchange to use in the generated config.",
|
|
180
|
-
default="BINANCE.UM",
|
|
181
|
-
show_default=True,
|
|
182
|
-
)
|
|
183
|
-
@click.option(
|
|
184
|
-
"--default-connector",
|
|
185
|
-
type=click.STRING,
|
|
186
|
-
help="Default connector to use in the generated config.",
|
|
187
|
-
default="ccxt",
|
|
188
|
-
show_default=True,
|
|
189
|
-
)
|
|
190
|
-
@click.option(
|
|
191
|
-
"--default-instruments",
|
|
192
|
-
type=click.STRING,
|
|
193
|
-
help="Default instruments to use in the generated config (comma-separated).",
|
|
194
|
-
default="BTCUSDT",
|
|
195
|
-
show_default=True,
|
|
196
|
-
)
|
|
197
181
|
def release(
|
|
198
182
|
directory: str,
|
|
199
183
|
strategy: str,
|
|
@@ -201,15 +185,12 @@ def release(
|
|
|
201
185
|
message: str | None,
|
|
202
186
|
commit: bool,
|
|
203
187
|
output_dir: str,
|
|
204
|
-
default_exchange: str,
|
|
205
|
-
default_connector: str,
|
|
206
|
-
default_instruments: str,
|
|
207
188
|
) -> None:
|
|
208
189
|
"""
|
|
209
190
|
Releases the strategy to a zip file.
|
|
210
191
|
|
|
211
192
|
The strategy can be specified in two ways:
|
|
212
|
-
1. As a strategy name (class name) - strategies are scanned in the given directory
|
|
193
|
+
1. As a strategy name (class name) - strategies are scanned in the given directory (NOT SUPPORTED ANYMORE !)
|
|
213
194
|
2. As a path to a config YAML file containing the strategy configuration in StrategyConfig format
|
|
214
195
|
|
|
215
196
|
If a strategy name is provided, a default configuration will be generated with:
|
|
@@ -228,9 +209,6 @@ def release(
|
|
|
228
209
|
"""
|
|
229
210
|
from .release import release_strategy
|
|
230
211
|
|
|
231
|
-
# Parse default instruments
|
|
232
|
-
instruments = [instr.strip() for instr in default_instruments.split(",")]
|
|
233
|
-
|
|
234
212
|
release_strategy(
|
|
235
213
|
directory=directory,
|
|
236
214
|
strategy_name=strategy,
|
|
@@ -238,9 +216,6 @@ def release(
|
|
|
238
216
|
message=message,
|
|
239
217
|
commit=commit,
|
|
240
218
|
output_dir=output_dir,
|
|
241
|
-
default_exchange=default_exchange,
|
|
242
|
-
default_connector=default_connector,
|
|
243
|
-
default_instruments=instruments,
|
|
244
219
|
)
|
|
245
220
|
|
|
246
221
|
|
qubx/cli/deploy.py
CHANGED
|
@@ -109,9 +109,7 @@ def ensure_poetry_lock_exists(output_dir: str) -> bool:
|
|
|
109
109
|
if not os.path.exists(poetry_lock_path):
|
|
110
110
|
logger.warning("poetry.lock not found in the zip file. Attempting to generate it.")
|
|
111
111
|
try:
|
|
112
|
-
subprocess.run(
|
|
113
|
-
["poetry", "lock", "--no-update"], cwd=output_dir, check=True, capture_output=True, text=True
|
|
114
|
-
)
|
|
112
|
+
subprocess.run(["poetry", "lock"], cwd=output_dir, check=True, capture_output=True, text=True)
|
|
115
113
|
return True
|
|
116
114
|
except subprocess.CalledProcessError as e:
|
|
117
115
|
logger.error(f"Failed to generate poetry.lock: {e.stderr}")
|
|
@@ -163,10 +161,10 @@ def setup_poetry_environment(output_dir: str) -> bool:
|
|
|
163
161
|
if var in env:
|
|
164
162
|
del env[var]
|
|
165
163
|
|
|
166
|
-
subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=
|
|
164
|
+
subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=False, text=True, env=env)
|
|
167
165
|
else:
|
|
168
166
|
# Normal case - not in a Poetry shell
|
|
169
|
-
subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=
|
|
167
|
+
subprocess.run(install_cmd, cwd=output_dir, check=True, capture_output=False, text=True)
|
|
170
168
|
|
|
171
169
|
# Verify that the virtual environment was created
|
|
172
170
|
venv_path = os.path.join(output_dir, ".venv")
|