Qubx 0.6.21__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.23__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 +16 -94
- qubx/backtester/broker.py +19 -38
- qubx/backtester/data.py +6 -7
- qubx/backtester/ome.py +13 -9
- qubx/backtester/runner.py +15 -4
- qubx/backtester/simulated_exchange.py +233 -0
- qubx/core/context.py +1 -0
- qubx/core/interfaces.py +29 -29
- 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/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +1 -1
- qubx/ta/indicators.pyx +5 -8
- qubx/utils/runner/runner.py +7 -3
- {qubx-0.6.21.dist-info → qubx-0.6.23.dist-info}/METADATA +1 -1
- {qubx-0.6.21.dist-info → qubx-0.6.23.dist-info}/RECORD +19 -18
- {qubx-0.6.21.dist-info → qubx-0.6.23.dist-info}/WHEEL +1 -1
- {qubx-0.6.21.dist-info → qubx-0.6.23.dist-info}/LICENSE +0 -0
- {qubx-0.6.21.dist-info → qubx-0.6.23.dist-info}/entry_points.txt +0 -0
qubx/backtester/account.py
CHANGED
|
@@ -1,54 +1,40 @@
|
|
|
1
|
-
from qubx import
|
|
2
|
-
from qubx.backtester.ome import OrdersManagementEngine
|
|
1
|
+
from qubx.backtester.simulated_exchange import ISimulatedExchange
|
|
3
2
|
from qubx.core.account import BasicAccountProcessor
|
|
4
3
|
from qubx.core.basics import (
|
|
5
|
-
ZERO_COSTS,
|
|
6
4
|
CtrlChannel,
|
|
7
5
|
Instrument,
|
|
8
6
|
Order,
|
|
9
7
|
Position,
|
|
10
8
|
Timestamped,
|
|
11
|
-
TransactionCostsCalculator,
|
|
12
9
|
dt_64,
|
|
13
10
|
)
|
|
14
|
-
from qubx.core.
|
|
15
|
-
from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
|
|
11
|
+
from qubx.core.series import OrderBook, Quote, Trade, TradeArray
|
|
16
12
|
from qubx.restorers import RestoredState
|
|
17
13
|
|
|
18
14
|
|
|
19
15
|
class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
20
|
-
ome: dict[Instrument, OrdersManagementEngine]
|
|
21
|
-
order_to_instrument: dict[str, Instrument]
|
|
22
|
-
|
|
23
16
|
_channel: CtrlChannel
|
|
24
|
-
|
|
25
|
-
_half_tick_size: dict[Instrument, float]
|
|
17
|
+
_exchange: ISimulatedExchange
|
|
26
18
|
|
|
27
19
|
def __init__(
|
|
28
20
|
self,
|
|
29
21
|
account_id: str,
|
|
22
|
+
exchange: ISimulatedExchange,
|
|
30
23
|
channel: CtrlChannel,
|
|
31
24
|
base_currency: str,
|
|
32
25
|
initial_capital: float,
|
|
33
|
-
time_provider: ITimeProvider,
|
|
34
|
-
tcc: TransactionCostsCalculator = ZERO_COSTS,
|
|
35
|
-
accurate_stop_orders_execution: bool = False,
|
|
36
26
|
restored_state: RestoredState | None = None,
|
|
37
27
|
) -> None:
|
|
38
28
|
super().__init__(
|
|
39
29
|
account_id=account_id,
|
|
40
|
-
time_provider=
|
|
30
|
+
time_provider=exchange.get_time_provider(),
|
|
41
31
|
base_currency=base_currency,
|
|
42
|
-
tcc=
|
|
32
|
+
tcc=exchange.get_transaction_costs_calculator(),
|
|
43
33
|
initial_capital=initial_capital,
|
|
44
34
|
)
|
|
45
|
-
|
|
46
|
-
self.
|
|
35
|
+
|
|
36
|
+
self._exchange = exchange
|
|
47
37
|
self._channel = channel
|
|
48
|
-
self._half_tick_size = {}
|
|
49
|
-
self._fill_stop_order_at_price = accurate_stop_orders_execution
|
|
50
|
-
if self._fill_stop_order_at_price:
|
|
51
|
-
logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
|
|
52
38
|
|
|
53
39
|
if restored_state is not None:
|
|
54
40
|
self._balances.update(restored_state.balances)
|
|
@@ -57,42 +43,25 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
57
43
|
_pos.reset_by_position(position)
|
|
58
44
|
|
|
59
45
|
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
60
|
-
|
|
61
|
-
ome = self.ome.get(instrument)
|
|
62
|
-
if ome is None:
|
|
63
|
-
raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
|
|
64
|
-
|
|
65
|
-
return {o.id: o for o in ome.get_open_orders()}
|
|
66
|
-
|
|
67
|
-
return {o.id: o for ome in self.ome.values() for o in ome.get_open_orders()}
|
|
46
|
+
return self._exchange.get_open_orders(instrument)
|
|
68
47
|
|
|
69
48
|
def get_position(self, instrument: Instrument) -> Position:
|
|
70
49
|
if instrument in self.positions:
|
|
71
50
|
return self.positions[instrument]
|
|
72
51
|
|
|
73
|
-
# -
|
|
74
|
-
self.ome[instrument] = OrdersManagementEngine(
|
|
75
|
-
instrument=instrument,
|
|
76
|
-
time_provider=self.time_provider,
|
|
77
|
-
tcc=self._tcc, # type: ignore
|
|
78
|
-
fill_stop_order_at_price=self._fill_stop_order_at_price,
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
# - initiolize empty position
|
|
52
|
+
# - initialize empty position
|
|
82
53
|
position = Position(instrument) # type: ignore
|
|
83
|
-
self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
|
|
84
54
|
self.attach_positions(position)
|
|
85
55
|
return self.positions[instrument]
|
|
86
56
|
|
|
87
57
|
def update_position_price(self, time: dt_64, instrument: Instrument, update: float | Timestamped) -> None:
|
|
58
|
+
self.get_position(instrument)
|
|
59
|
+
|
|
88
60
|
super().update_position_price(time, instrument, update)
|
|
89
61
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
# - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
|
|
94
|
-
# - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
|
|
95
|
-
quote = update if isinstance(update, Quote) else self.emulate_quote_from_data(instrument, time, update)
|
|
62
|
+
quote = (
|
|
63
|
+
update if isinstance(update, Quote) else self._exchange.emulate_quote_from_data(instrument, time, update)
|
|
64
|
+
)
|
|
96
65
|
if quote is None:
|
|
97
66
|
return
|
|
98
67
|
|
|
@@ -106,56 +75,9 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
106
75
|
|
|
107
76
|
super().process_market_data(time, instrument, update)
|
|
108
77
|
|
|
109
|
-
def process_order(self, order: Order, update_locked_value: bool = True) -> None:
|
|
110
|
-
_new = order.status == "NEW"
|
|
111
|
-
_open = order.status == "OPEN"
|
|
112
|
-
_cancel = order.status == "CANCELED"
|
|
113
|
-
_closed = order.status == "CLOSED"
|
|
114
|
-
if _new or _open:
|
|
115
|
-
self.order_to_instrument[order.id] = order.instrument
|
|
116
|
-
if (_cancel or _closed) and order.id in self.order_to_instrument:
|
|
117
|
-
self.order_to_instrument.pop(order.id)
|
|
118
|
-
return super().process_order(order, update_locked_value)
|
|
119
|
-
|
|
120
|
-
def emulate_quote_from_data(
|
|
121
|
-
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
|
|
122
|
-
) -> Quote | None:
|
|
123
|
-
if instrument not in self._half_tick_size:
|
|
124
|
-
_ = self.get_position(instrument)
|
|
125
|
-
|
|
126
|
-
if isinstance(data, Quote):
|
|
127
|
-
return data
|
|
128
|
-
|
|
129
|
-
elif isinstance(data, Trade):
|
|
130
|
-
_ts2 = self._half_tick_size[instrument]
|
|
131
|
-
if data.side == 1: # type: ignore
|
|
132
|
-
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
133
|
-
else:
|
|
134
|
-
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
135
|
-
|
|
136
|
-
elif isinstance(data, Bar):
|
|
137
|
-
_ts2 = self._half_tick_size[instrument]
|
|
138
|
-
return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
|
|
139
|
-
|
|
140
|
-
elif isinstance(data, OrderBook):
|
|
141
|
-
return data.to_quote()
|
|
142
|
-
|
|
143
|
-
elif isinstance(data, float):
|
|
144
|
-
_ts2 = self._half_tick_size[instrument]
|
|
145
|
-
return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
|
|
146
|
-
|
|
147
|
-
else:
|
|
148
|
-
return None
|
|
149
|
-
|
|
150
78
|
def _process_new_data(self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray) -> None:
|
|
151
|
-
|
|
152
|
-
if ome is None:
|
|
153
|
-
logger.warning(f"ExchangeService:update :: No OME configured for '{instrument}' yet !")
|
|
154
|
-
return
|
|
155
|
-
for r in ome.process_market_data(data):
|
|
79
|
+
for r in self._exchange.process_market_data(instrument, data):
|
|
156
80
|
if r.exec is not None:
|
|
157
|
-
if r.order.id in self.order_to_instrument:
|
|
158
|
-
self.order_to_instrument.pop(r.order.id)
|
|
159
81
|
# - process methods will be called from stg context
|
|
160
82
|
self._channel.send((instrument, "order", r.order, False))
|
|
161
83
|
self._channel.send((instrument, "deals", [r.exec], False))
|
qubx/backtester/broker.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from qubx.backtester.ome import
|
|
1
|
+
from qubx.backtester.ome import SimulatedExecutionReport
|
|
2
|
+
from qubx.backtester.simulated_exchange import ISimulatedExchange
|
|
2
3
|
from qubx.core.basics import (
|
|
3
4
|
CtrlChannel,
|
|
4
5
|
Instrument,
|
|
@@ -13,16 +14,17 @@ class SimulatedBroker(IBroker):
|
|
|
13
14
|
channel: CtrlChannel
|
|
14
15
|
|
|
15
16
|
_account: SimulatedAccountProcessor
|
|
17
|
+
_exchange: ISimulatedExchange
|
|
16
18
|
|
|
17
19
|
def __init__(
|
|
18
20
|
self,
|
|
19
21
|
channel: CtrlChannel,
|
|
20
22
|
account: SimulatedAccountProcessor,
|
|
21
|
-
|
|
23
|
+
simulated_exchange: ISimulatedExchange,
|
|
22
24
|
) -> None:
|
|
23
25
|
self.channel = channel
|
|
24
26
|
self._account = account
|
|
25
|
-
self.
|
|
27
|
+
self._exchange = simulated_exchange
|
|
26
28
|
|
|
27
29
|
@property
|
|
28
30
|
def is_simulated_trading(self) -> bool:
|
|
@@ -39,22 +41,12 @@ class SimulatedBroker(IBroker):
|
|
|
39
41
|
time_in_force: str = "gtc",
|
|
40
42
|
**options,
|
|
41
43
|
) -> Order:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
report = ome.place_order(
|
|
48
|
-
order_side.upper(), # type: ignore
|
|
49
|
-
order_type.upper(), # type: ignore
|
|
50
|
-
amount,
|
|
51
|
-
price,
|
|
52
|
-
client_id,
|
|
53
|
-
time_in_force,
|
|
54
|
-
**options,
|
|
44
|
+
# - place order at exchange and send exec report to data channel
|
|
45
|
+
self._send_execution_report(
|
|
46
|
+
report := self._exchange.place_order(
|
|
47
|
+
instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
48
|
+
)
|
|
55
49
|
)
|
|
56
|
-
|
|
57
|
-
self._send_exec_report(instrument, report)
|
|
58
50
|
return report.order
|
|
59
51
|
|
|
60
52
|
def send_order_async(
|
|
@@ -71,22 +63,8 @@ class SimulatedBroker(IBroker):
|
|
|
71
63
|
self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
|
|
72
64
|
|
|
73
65
|
def cancel_order(self, order_id: str) -> Order | None:
|
|
74
|
-
|
|
75
|
-
if
|
|
76
|
-
raise ValueError(f"ExchangeService:cancel_order :: can't find order with id = '{order_id}'!")
|
|
77
|
-
|
|
78
|
-
ome = self._account.ome.get(instrument)
|
|
79
|
-
if ome is None:
|
|
80
|
-
raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument}'!")
|
|
81
|
-
|
|
82
|
-
# - cancel order in OME and remove from the map to free memory
|
|
83
|
-
order_update = ome.cancel_order(order_id)
|
|
84
|
-
if order_update is None:
|
|
85
|
-
return None
|
|
86
|
-
|
|
87
|
-
self._send_exec_report(instrument, order_update)
|
|
88
|
-
|
|
89
|
-
return order_update.order
|
|
66
|
+
self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
|
|
67
|
+
return order_update.order if order_update is not None else None
|
|
90
68
|
|
|
91
69
|
def cancel_orders(self, instrument: Instrument) -> None:
|
|
92
70
|
raise NotImplementedError("Not implemented yet")
|
|
@@ -94,10 +72,13 @@ class SimulatedBroker(IBroker):
|
|
|
94
72
|
def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
|
|
95
73
|
raise NotImplementedError("Not implemented yet")
|
|
96
74
|
|
|
97
|
-
def
|
|
98
|
-
|
|
75
|
+
def _send_execution_report(self, report: SimulatedExecutionReport | None):
|
|
76
|
+
if report is None:
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
self.channel.send((report.instrument, "order", report.order, False))
|
|
99
80
|
if report.exec is not None:
|
|
100
|
-
self.channel.send((instrument, "deals", [report.exec], False))
|
|
81
|
+
self.channel.send((report.instrument, "deals", [report.exec], False))
|
|
101
82
|
|
|
102
83
|
def exchange(self) -> str:
|
|
103
|
-
return self.
|
|
84
|
+
return self._exchange.exchange_id
|
qubx/backtester/data.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
5
|
import pandas as pd
|
|
@@ -30,9 +30,8 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
30
30
|
|
|
31
31
|
_scheduler: BasicScheduler
|
|
32
32
|
_account: SimulatedAccountProcessor
|
|
33
|
-
_last_quotes:
|
|
33
|
+
_last_quotes: dict[Instrument, Quote | None]
|
|
34
34
|
_readers: dict[str, DataReader]
|
|
35
|
-
_scheduler: BasicScheduler
|
|
36
35
|
_pregenerated_signals: dict[Instrument, pd.Series | pd.DataFrame]
|
|
37
36
|
_to_process: dict[Instrument, list]
|
|
38
37
|
_data_source: IterableSimulationData
|
|
@@ -143,7 +142,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
143
142
|
if h_data:
|
|
144
143
|
# _s_type = DataType.from_str(subscription_type)[0]
|
|
145
144
|
last_update = h_data[-1]
|
|
146
|
-
if last_quote := self._account.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
|
|
145
|
+
if last_quote := self._account._exchange.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
|
|
147
146
|
# - send historical data to the channel
|
|
148
147
|
self.channel.send((i, subscription_type, h_data, True))
|
|
149
148
|
|
|
@@ -151,7 +150,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
151
150
|
self._last_quotes[i] = last_quote
|
|
152
151
|
|
|
153
152
|
# - also need to pass this quote to OME !
|
|
154
|
-
self._account.
|
|
153
|
+
self._account.process_market_data(last_quote.time, i, last_quote) # type: ignore
|
|
155
154
|
|
|
156
155
|
logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
|
|
157
156
|
|
|
@@ -266,7 +265,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
266
265
|
cc.send((instrument, "event", {"order": sigs[0][1]}, False))
|
|
267
266
|
sigs.pop(0)
|
|
268
267
|
|
|
269
|
-
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
268
|
+
if q := self._account._exchange.emulate_quote_from_data(instrument, t, data):
|
|
270
269
|
self._last_quotes[instrument] = q
|
|
271
270
|
|
|
272
271
|
self.time_provider.set_time(t)
|
|
@@ -284,7 +283,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
284
283
|
self.time_provider.set_time(_next_exp_time)
|
|
285
284
|
self._scheduler.check_and_run_tasks()
|
|
286
285
|
|
|
287
|
-
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
286
|
+
if q := self._account._exchange.emulate_quote_from_data(instrument, t, data):
|
|
288
287
|
self._last_quotes[instrument] = q
|
|
289
288
|
|
|
290
289
|
self.time_provider.set_time(t)
|
qubx/backtester/ome.py
CHANGED
|
@@ -27,7 +27,8 @@ from qubx.core.series import OrderBook, Quote, Trade, TradeArray
|
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
30
|
-
class
|
|
30
|
+
class SimulatedExecutionReport:
|
|
31
|
+
instrument: Instrument
|
|
31
32
|
timestamp: dt_64
|
|
32
33
|
order: Order
|
|
33
34
|
exec: Deal | None
|
|
@@ -91,7 +92,7 @@ class OrdersManagementEngine:
|
|
|
91
92
|
def get_open_orders(self) -> list[Order]:
|
|
92
93
|
return list(self.active_orders.values()) + list(self.stop_orders.values())
|
|
93
94
|
|
|
94
|
-
def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[
|
|
95
|
+
def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[SimulatedExecutionReport]:
|
|
95
96
|
"""
|
|
96
97
|
Processes the new market data (quote, trade or trades array) and simulates the execution of pending orders.
|
|
97
98
|
"""
|
|
@@ -173,7 +174,7 @@ class OrdersManagementEngine:
|
|
|
173
174
|
client_id: str | None = None,
|
|
174
175
|
time_in_force: str = "gtc",
|
|
175
176
|
**options,
|
|
176
|
-
) ->
|
|
177
|
+
) -> SimulatedExecutionReport:
|
|
177
178
|
if self.bbo is None:
|
|
178
179
|
raise ExchangeError(f"Simulator is not ready for order management - no quote for {self.instrument.symbol}")
|
|
179
180
|
|
|
@@ -200,7 +201,7 @@ class OrdersManagementEngine:
|
|
|
200
201
|
def _dbg(self, message, **kwargs) -> None:
|
|
201
202
|
logger.debug(f" [<y>OME</y>(<g>{self.instrument}</g>)] :: {message}", **kwargs)
|
|
202
203
|
|
|
203
|
-
def _process_order(self, timestamp: dt_64, order: Order) ->
|
|
204
|
+
def _process_order(self, timestamp: dt_64, order: Order) -> SimulatedExecutionReport:
|
|
204
205
|
if order.status in ["CLOSED", "CANCELED"]:
|
|
205
206
|
raise InvalidOrder(f"Order {order.id} is already closed or canceled.")
|
|
206
207
|
|
|
@@ -273,12 +274,15 @@ class OrdersManagementEngine:
|
|
|
273
274
|
self.active_orders[order.id] = order
|
|
274
275
|
|
|
275
276
|
self._dbg(f"registered {order.id} {order.type} {order.side} {order.quantity} {order.price}")
|
|
276
|
-
return
|
|
277
|
+
return SimulatedExecutionReport(self.instrument, timestamp, order, None)
|
|
277
278
|
|
|
278
|
-
def _execute_order(
|
|
279
|
+
def _execute_order(
|
|
280
|
+
self, timestamp: dt_64, exec_price: float, order: Order, taker: bool
|
|
281
|
+
) -> SimulatedExecutionReport:
|
|
279
282
|
order.status = "CLOSED"
|
|
280
283
|
self._dbg(f"<red>{order.id}</red> {order.type} {order.side} {order.quantity} executed at {exec_price}")
|
|
281
|
-
return
|
|
284
|
+
return SimulatedExecutionReport(
|
|
285
|
+
self.instrument,
|
|
282
286
|
timestamp,
|
|
283
287
|
order,
|
|
284
288
|
Deal(
|
|
@@ -322,7 +326,7 @@ class OrdersManagementEngine:
|
|
|
322
326
|
f"Stop price would trigger immediately: STOP_MARKET {order_side} {amount} of {self.instrument.symbol} at {price} | market: {c_ask} / {c_bid}"
|
|
323
327
|
)
|
|
324
328
|
|
|
325
|
-
def cancel_order(self, order_id: str) ->
|
|
329
|
+
def cancel_order(self, order_id: str) -> SimulatedExecutionReport | None:
|
|
326
330
|
# - check limit orders
|
|
327
331
|
if order_id in self.active_orders:
|
|
328
332
|
order = self.active_orders.pop(order_id)
|
|
@@ -346,7 +350,7 @@ class OrdersManagementEngine:
|
|
|
346
350
|
|
|
347
351
|
order.status = "CANCELED"
|
|
348
352
|
self._dbg(f"{order.id} {order.type} {order.side} {order.quantity} canceled")
|
|
349
|
-
return
|
|
353
|
+
return SimulatedExecutionReport(self.instrument, self.time_service.time(), order, None)
|
|
350
354
|
|
|
351
355
|
def __str__(self) -> str:
|
|
352
356
|
_a, _b = True, True
|
qubx/backtester/runner.py
CHANGED
|
@@ -17,6 +17,7 @@ from qubx.pandaz.utils import _frame_to_str
|
|
|
17
17
|
from .account import SimulatedAccountProcessor
|
|
18
18
|
from .broker import SimulatedBroker
|
|
19
19
|
from .data import SimulatedDataProvider
|
|
20
|
+
from .simulated_exchange import get_simulated_exchange
|
|
20
21
|
from .utils import (
|
|
21
22
|
SetupTypes,
|
|
22
23
|
SignalsProxy,
|
|
@@ -176,17 +177,25 @@ class SimulationRunner:
|
|
|
176
177
|
f"[<y>simulator</y>] :: Preparing simulated trading on <g>{self.setup.exchange.upper()}</g> for {self.setup.capital} {self.setup.base_currency}..."
|
|
177
178
|
)
|
|
178
179
|
|
|
180
|
+
# - create simulated exchange:
|
|
181
|
+
# - we can use different emulations of real exchanges features in future here: for Binance, Bybit, InteractiveBrokers, etc.
|
|
182
|
+
# - for now we use simple basic simulated exchange implementation
|
|
183
|
+
simulated_exchange = get_simulated_exchange(
|
|
184
|
+
self.setup.exchange, simulated_clock, tcc, self.setup.accurate_stop_orders_execution
|
|
185
|
+
)
|
|
186
|
+
|
|
179
187
|
account = SimulatedAccountProcessor(
|
|
180
188
|
account_id=self.account_id,
|
|
189
|
+
exchange=simulated_exchange,
|
|
181
190
|
channel=channel,
|
|
182
191
|
base_currency=self.setup.base_currency,
|
|
183
192
|
initial_capital=self.setup.capital,
|
|
184
|
-
time_provider=simulated_clock,
|
|
185
|
-
tcc=tcc,
|
|
186
|
-
accurate_stop_orders_execution=self.setup.accurate_stop_orders_execution,
|
|
187
193
|
)
|
|
188
194
|
scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
|
|
189
|
-
|
|
195
|
+
|
|
196
|
+
# - broker is order's interface to the exchange
|
|
197
|
+
broker = SimulatedBroker(channel, account, simulated_exchange)
|
|
198
|
+
|
|
190
199
|
data_provider = SimulatedDataProvider(
|
|
191
200
|
exchange_id=self.setup.exchange,
|
|
192
201
|
channel=channel,
|
|
@@ -196,8 +205,10 @@ class SimulationRunner:
|
|
|
196
205
|
readers=self.data_config.data_providers,
|
|
197
206
|
open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
|
|
198
207
|
)
|
|
208
|
+
|
|
199
209
|
# - get aux data provider
|
|
200
210
|
_aux_data = self.data_config.get_timeguarded_aux_reader(simulated_clock)
|
|
211
|
+
|
|
201
212
|
# - it will store simulation results into memory
|
|
202
213
|
logs_writer = InMemoryLogsWriter(self.account_id, self.setup.name, "0")
|
|
203
214
|
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
2
|
+
|
|
3
|
+
from qubx import logger
|
|
4
|
+
from qubx.backtester.ome import OrdersManagementEngine, SimulatedExecutionReport
|
|
5
|
+
from qubx.core.basics import (
|
|
6
|
+
ZERO_COSTS,
|
|
7
|
+
Instrument,
|
|
8
|
+
ITimeProvider,
|
|
9
|
+
Order,
|
|
10
|
+
Timestamped,
|
|
11
|
+
TransactionCostsCalculator,
|
|
12
|
+
dt_64,
|
|
13
|
+
)
|
|
14
|
+
from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ISimulatedExchange:
|
|
18
|
+
"""
|
|
19
|
+
Generic interface for simulated exchange.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
exchange_id: str
|
|
23
|
+
_half_tick_size: dict[Instrument, float]
|
|
24
|
+
|
|
25
|
+
def __init__(self, exchange_id: str):
|
|
26
|
+
self.exchange_id = exchange_id.upper()
|
|
27
|
+
self._half_tick_size = {}
|
|
28
|
+
|
|
29
|
+
def get_time_provider(self) -> ITimeProvider: ...
|
|
30
|
+
|
|
31
|
+
def get_transaction_costs_calculator(self) -> TransactionCostsCalculator: ...
|
|
32
|
+
|
|
33
|
+
def place_order(
|
|
34
|
+
self,
|
|
35
|
+
instrument: Instrument,
|
|
36
|
+
order_side: str,
|
|
37
|
+
order_type: str,
|
|
38
|
+
amount: float,
|
|
39
|
+
price: float | None = None,
|
|
40
|
+
client_id: str | None = None,
|
|
41
|
+
time_in_force: str = "gtc",
|
|
42
|
+
**options,
|
|
43
|
+
) -> SimulatedExecutionReport: ...
|
|
44
|
+
|
|
45
|
+
def cancel_order(self, order_id: str) -> SimulatedExecutionReport | None: ...
|
|
46
|
+
|
|
47
|
+
def get_open_orders(self, instrument: Instrument | None = None) -> dict[str, Order]: ...
|
|
48
|
+
|
|
49
|
+
def process_market_data(
|
|
50
|
+
self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray
|
|
51
|
+
) -> Generator[SimulatedExecutionReport]: ...
|
|
52
|
+
|
|
53
|
+
def emulate_quote_from_data(
|
|
54
|
+
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
|
|
55
|
+
) -> Quote | None:
|
|
56
|
+
"""
|
|
57
|
+
Emulate quote from data.
|
|
58
|
+
|
|
59
|
+
TODO: we need to get rid of this method in the future
|
|
60
|
+
"""
|
|
61
|
+
if instrument not in self._half_tick_size:
|
|
62
|
+
self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
|
|
63
|
+
|
|
64
|
+
if isinstance(data, Quote):
|
|
65
|
+
return data
|
|
66
|
+
|
|
67
|
+
elif isinstance(data, Trade):
|
|
68
|
+
_ts2 = self._half_tick_size[instrument]
|
|
69
|
+
if data.side == 1: # type: ignore
|
|
70
|
+
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
71
|
+
else:
|
|
72
|
+
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
73
|
+
|
|
74
|
+
elif isinstance(data, Bar):
|
|
75
|
+
_ts2 = self._half_tick_size[instrument]
|
|
76
|
+
return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
|
|
77
|
+
|
|
78
|
+
elif isinstance(data, OrderBook):
|
|
79
|
+
return data.to_quote()
|
|
80
|
+
|
|
81
|
+
elif isinstance(data, float):
|
|
82
|
+
_ts2 = self._half_tick_size[instrument]
|
|
83
|
+
return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
|
|
84
|
+
|
|
85
|
+
else:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class BasicSimulatedExchange(ISimulatedExchange):
|
|
90
|
+
"""
|
|
91
|
+
Basic implementation of generic crypto exchange.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
_ome: dict[Instrument, OrdersManagementEngine]
|
|
95
|
+
_order_to_instrument: dict[str, Instrument]
|
|
96
|
+
_fill_stop_order_at_price: bool
|
|
97
|
+
_time_provider: ITimeProvider
|
|
98
|
+
_tcc: TransactionCostsCalculator
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
exchange_id: str,
|
|
103
|
+
time_provider: ITimeProvider,
|
|
104
|
+
tcc: TransactionCostsCalculator = ZERO_COSTS,
|
|
105
|
+
accurate_stop_orders_execution: bool = False,
|
|
106
|
+
):
|
|
107
|
+
super().__init__(exchange_id)
|
|
108
|
+
self._ome = {}
|
|
109
|
+
self._order_to_instrument = {}
|
|
110
|
+
self._half_tick_size = {}
|
|
111
|
+
self._fill_stop_order_at_price = accurate_stop_orders_execution
|
|
112
|
+
self._time_provider = time_provider
|
|
113
|
+
self._tcc = tcc
|
|
114
|
+
if self._fill_stop_order_at_price:
|
|
115
|
+
logger.info(
|
|
116
|
+
f"[<y>{self.__class__.__name__}</y>] :: emulation of stop orders executions at exact price is ON"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def get_time_provider(self) -> ITimeProvider:
|
|
120
|
+
return self._time_provider
|
|
121
|
+
|
|
122
|
+
def get_transaction_costs_calculator(self) -> TransactionCostsCalculator:
|
|
123
|
+
return self._tcc
|
|
124
|
+
|
|
125
|
+
def place_order(
|
|
126
|
+
self,
|
|
127
|
+
instrument: Instrument,
|
|
128
|
+
order_side: str,
|
|
129
|
+
order_type: str,
|
|
130
|
+
amount: float,
|
|
131
|
+
price: float | None = None,
|
|
132
|
+
client_id: str | None = None,
|
|
133
|
+
time_in_force: str = "gtc",
|
|
134
|
+
**options,
|
|
135
|
+
) -> SimulatedExecutionReport:
|
|
136
|
+
# - try to place order in OME
|
|
137
|
+
return self._get_ome(instrument).place_order(
|
|
138
|
+
order_side.upper(), # type: ignore
|
|
139
|
+
order_type.upper(), # type: ignore
|
|
140
|
+
amount,
|
|
141
|
+
price,
|
|
142
|
+
client_id,
|
|
143
|
+
time_in_force,
|
|
144
|
+
**options,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def cancel_order(self, order_id: str) -> SimulatedExecutionReport | None:
|
|
148
|
+
# - first check in active orders
|
|
149
|
+
instrument = self._order_to_instrument.get(order_id)
|
|
150
|
+
|
|
151
|
+
if instrument is None:
|
|
152
|
+
# - if not found in active orders, check in each OME
|
|
153
|
+
for o in self._ome.values():
|
|
154
|
+
for order in o.get_open_orders():
|
|
155
|
+
if order.id == order_id:
|
|
156
|
+
return self._process_ome_response(o.cancel_order(order_id))
|
|
157
|
+
|
|
158
|
+
logger.error(
|
|
159
|
+
f"[<y>{self.__class__.__name__}</y>] :: cancel_order :: can't find order with id = 'ValueError{order_id}'!"
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
ome = self._ome.get(instrument)
|
|
164
|
+
if ome is None:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"{self.__class__.__name__}</y>] :: cancel_order :: No OME created for '{instrument}' - fatal error!"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# - cancel order in OME and remove from the map to free memory
|
|
170
|
+
return self._process_ome_response(ome.cancel_order(order_id))
|
|
171
|
+
|
|
172
|
+
def _process_ome_response(self, report: SimulatedExecutionReport | None) -> SimulatedExecutionReport | None:
|
|
173
|
+
if report is not None:
|
|
174
|
+
_order = report.order
|
|
175
|
+
_new = _order.status == "NEW"
|
|
176
|
+
_open = _order.status == "OPEN"
|
|
177
|
+
_cancel = _order.status == "CANCELED"
|
|
178
|
+
_closed = _order.status == "CLOSED"
|
|
179
|
+
|
|
180
|
+
if _new or _open:
|
|
181
|
+
self._order_to_instrument[_order.id] = _order.instrument
|
|
182
|
+
|
|
183
|
+
if (_cancel or _closed) and _order.id in self._order_to_instrument:
|
|
184
|
+
self._order_to_instrument.pop(_order.id)
|
|
185
|
+
|
|
186
|
+
return report
|
|
187
|
+
|
|
188
|
+
def get_open_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
189
|
+
if instrument is not None:
|
|
190
|
+
ome = self._get_ome(instrument)
|
|
191
|
+
return {o.id: o for o in ome.get_open_orders()}
|
|
192
|
+
|
|
193
|
+
return {o.id: o for ome in self._ome.values() for o in ome.get_open_orders()}
|
|
194
|
+
|
|
195
|
+
def _get_ome(self, instrument: Instrument) -> OrdersManagementEngine:
|
|
196
|
+
if (ome := self._ome.get(instrument)) is None:
|
|
197
|
+
self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
|
|
198
|
+
# - create order management engine for instrument
|
|
199
|
+
self._ome[instrument] = (
|
|
200
|
+
ome := OrdersManagementEngine(
|
|
201
|
+
instrument=instrument,
|
|
202
|
+
time_provider=self._time_provider,
|
|
203
|
+
tcc=self._tcc, # type: ignore
|
|
204
|
+
fill_stop_order_at_price=self._fill_stop_order_at_price,
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
return ome
|
|
208
|
+
|
|
209
|
+
def process_market_data(
|
|
210
|
+
self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray
|
|
211
|
+
) -> Generator[SimulatedExecutionReport]:
|
|
212
|
+
ome = self._get_ome(instrument)
|
|
213
|
+
|
|
214
|
+
for r in ome.process_market_data(data):
|
|
215
|
+
if r.exec is not None:
|
|
216
|
+
if r.order.id in self._order_to_instrument:
|
|
217
|
+
self._order_to_instrument.pop(r.order.id)
|
|
218
|
+
yield r
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_simulated_exchange(
|
|
222
|
+
exchange_name: str,
|
|
223
|
+
time_provider: ITimeProvider,
|
|
224
|
+
tcc: TransactionCostsCalculator,
|
|
225
|
+
accurate_stop_orders_execution=False,
|
|
226
|
+
) -> ISimulatedExchange:
|
|
227
|
+
"""
|
|
228
|
+
Factory function to create different types of simulated exchanges based on it's name etc
|
|
229
|
+
Now it supports only basic exchange that fits for most cases of crypto trading.
|
|
230
|
+
"""
|
|
231
|
+
return BasicSimulatedExchange(
|
|
232
|
+
exchange_name, time_provider, tcc, accurate_stop_orders_execution=accurate_stop_orders_execution
|
|
233
|
+
)
|
qubx/core/context.py
CHANGED
|
@@ -550,6 +550,7 @@ class StrategyContext(IStrategyContext):
|
|
|
550
550
|
if self._lifecycle_notifier:
|
|
551
551
|
self._lifecycle_notifier.notify_error(self._strategy_name, e)
|
|
552
552
|
# Don't stop the channel here, let it continue processing
|
|
553
|
+
|
|
553
554
|
logger.info("[StrategyContext] :: Market data processing stopped")
|
|
554
555
|
|
|
555
556
|
def __instantiate_strategy(self, strategy: IStrategy, config: dict[str, Any] | None) -> IStrategy:
|
qubx/core/interfaces.py
CHANGED
|
@@ -14,7 +14,7 @@ This module includes:
|
|
|
14
14
|
|
|
15
15
|
import traceback
|
|
16
16
|
from dataclasses import dataclass
|
|
17
|
-
from typing import Any, Callable,
|
|
17
|
+
from typing import Any, Callable, Literal, Protocol
|
|
18
18
|
|
|
19
19
|
import numpy as np
|
|
20
20
|
import pandas as pd
|
|
@@ -49,24 +49,24 @@ RemovalPolicy = Literal["close", "wait_for_close", "wait_for_change"]
|
|
|
49
49
|
class ITradeDataExport:
|
|
50
50
|
"""Interface for exporting trading data to external systems."""
|
|
51
51
|
|
|
52
|
-
def export_signals(self, time: dt_64, signals:
|
|
52
|
+
def export_signals(self, time: dt_64, signals: list[Signal], account: "IAccountViewer") -> None:
|
|
53
53
|
"""
|
|
54
54
|
Export signals to an external system.
|
|
55
55
|
|
|
56
56
|
Args:
|
|
57
57
|
time: Timestamp when the signals were generated
|
|
58
|
-
signals:
|
|
58
|
+
signals: list of signals to export
|
|
59
59
|
account: Account viewer to get account information like total capital, leverage, etc.
|
|
60
60
|
"""
|
|
61
61
|
pass
|
|
62
62
|
|
|
63
|
-
def export_target_positions(self, time: dt_64, targets:
|
|
63
|
+
def export_target_positions(self, time: dt_64, targets: list[TargetPosition], account: "IAccountViewer") -> None:
|
|
64
64
|
"""
|
|
65
65
|
Export target positions to an external system.
|
|
66
66
|
|
|
67
67
|
Args:
|
|
68
68
|
time: Timestamp when the target positions were generated
|
|
69
|
-
targets:
|
|
69
|
+
targets: list of target positions to export
|
|
70
70
|
account: Account viewer to get account information like total capital, leverage, etc.
|
|
71
71
|
"""
|
|
72
72
|
pass
|
|
@@ -377,7 +377,7 @@ class IDataProvider:
|
|
|
377
377
|
def subscribe(
|
|
378
378
|
self,
|
|
379
379
|
subscription_type: str,
|
|
380
|
-
instruments:
|
|
380
|
+
instruments: set[Instrument],
|
|
381
381
|
reset: bool = False,
|
|
382
382
|
) -> None:
|
|
383
383
|
"""
|
|
@@ -390,7 +390,7 @@ class IDataProvider:
|
|
|
390
390
|
"""
|
|
391
391
|
...
|
|
392
392
|
|
|
393
|
-
def unsubscribe(self, subscription_type: str | None, instruments:
|
|
393
|
+
def unsubscribe(self, subscription_type: str | None, instruments: set[Instrument]) -> None:
|
|
394
394
|
"""
|
|
395
395
|
Unsubscribe from market data for a list of instruments.
|
|
396
396
|
|
|
@@ -413,7 +413,7 @@ class IDataProvider:
|
|
|
413
413
|
"""
|
|
414
414
|
...
|
|
415
415
|
|
|
416
|
-
def get_subscriptions(self, instrument: Instrument | None = None) ->
|
|
416
|
+
def get_subscriptions(self, instrument: Instrument | None = None) -> list[str]:
|
|
417
417
|
"""
|
|
418
418
|
Get all subscriptions for an instrument.
|
|
419
419
|
|
|
@@ -421,11 +421,11 @@ class IDataProvider:
|
|
|
421
421
|
instrument (optional): Instrument to get subscriptions for. If None, all subscriptions are returned.
|
|
422
422
|
|
|
423
423
|
Returns:
|
|
424
|
-
|
|
424
|
+
list[str]: list of subscriptions
|
|
425
425
|
"""
|
|
426
426
|
...
|
|
427
427
|
|
|
428
|
-
def get_subscribed_instruments(self, subscription_type: str | None = None) ->
|
|
428
|
+
def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
|
|
429
429
|
"""
|
|
430
430
|
Get a list of instruments that are subscribed to a specific subscription type.
|
|
431
431
|
|
|
@@ -433,11 +433,11 @@ class IDataProvider:
|
|
|
433
433
|
subscription_type: Type of subscription to filter by (optional)
|
|
434
434
|
|
|
435
435
|
Returns:
|
|
436
|
-
|
|
436
|
+
list[Instrument]: list of subscribed instruments
|
|
437
437
|
"""
|
|
438
438
|
...
|
|
439
439
|
|
|
440
|
-
def warmup(self, configs:
|
|
440
|
+
def warmup(self, configs: dict[tuple[str, Instrument], str]) -> None:
|
|
441
441
|
"""
|
|
442
442
|
Run warmup for subscriptions.
|
|
443
443
|
|
|
@@ -522,7 +522,7 @@ class IMarketManager(ITimeProvider):
|
|
|
522
522
|
sub_type: The subscription type of data to get
|
|
523
523
|
|
|
524
524
|
Returns:
|
|
525
|
-
|
|
525
|
+
list[Any]: The data
|
|
526
526
|
"""
|
|
527
527
|
...
|
|
528
528
|
|
|
@@ -542,7 +542,7 @@ class IMarketManager(ITimeProvider):
|
|
|
542
542
|
"""Get list of subscribed instruments.
|
|
543
543
|
|
|
544
544
|
Returns:
|
|
545
|
-
list[Instrument]:
|
|
545
|
+
list[Instrument]: list of subscribed instruments
|
|
546
546
|
"""
|
|
547
547
|
...
|
|
548
548
|
|
|
@@ -683,9 +683,9 @@ class IUniverseManager:
|
|
|
683
683
|
"""Set the trading universe.
|
|
684
684
|
|
|
685
685
|
Args:
|
|
686
|
-
instruments:
|
|
687
|
-
skip_callback:
|
|
688
|
-
if_has_position_then:
|
|
686
|
+
instruments: list of instruments in the universe
|
|
687
|
+
skip_callback: skip callback to the strategy
|
|
688
|
+
if_has_position_then: what to do if the instrument has a position
|
|
689
689
|
- "close" (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
|
|
690
690
|
- "wait_for_close" - keep instrument and it's position until it's closed from strategy (or risk management), then remove instrument from strategy
|
|
691
691
|
- "wait_for_change" - keep instrument and position until strategy would try to change it - then close position and remove instrument
|
|
@@ -752,7 +752,7 @@ class IUniverseManager:
|
|
|
752
752
|
class ISubscriptionManager:
|
|
753
753
|
"""Manages subscriptions."""
|
|
754
754
|
|
|
755
|
-
def subscribe(self, subscription_type: str, instruments:
|
|
755
|
+
def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
|
|
756
756
|
"""Subscribe to market data for an instrument.
|
|
757
757
|
|
|
758
758
|
Args:
|
|
@@ -761,7 +761,7 @@ class ISubscriptionManager:
|
|
|
761
761
|
"""
|
|
762
762
|
...
|
|
763
763
|
|
|
764
|
-
def unsubscribe(self, subscription_type: str, instruments:
|
|
764
|
+
def unsubscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
|
|
765
765
|
"""Unsubscribe from market data for an instrument.
|
|
766
766
|
|
|
767
767
|
Args:
|
|
@@ -799,7 +799,7 @@ class ISubscriptionManager:
|
|
|
799
799
|
"""
|
|
800
800
|
...
|
|
801
801
|
|
|
802
|
-
def get_subscriptions(self, instrument: Instrument | None = None) ->
|
|
802
|
+
def get_subscriptions(self, instrument: Instrument | None = None) -> list[str]:
|
|
803
803
|
"""
|
|
804
804
|
Get all subscriptions for an instrument.
|
|
805
805
|
|
|
@@ -807,11 +807,11 @@ class ISubscriptionManager:
|
|
|
807
807
|
instrument: Instrument to get subscriptions for (optional)
|
|
808
808
|
|
|
809
809
|
Returns:
|
|
810
|
-
|
|
810
|
+
list[str]: list of subscriptions
|
|
811
811
|
"""
|
|
812
812
|
...
|
|
813
813
|
|
|
814
|
-
def get_subscribed_instruments(self, subscription_type: str | None = None) ->
|
|
814
|
+
def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
|
|
815
815
|
"""
|
|
816
816
|
Get a list of instruments that are subscribed to a specific subscription type.
|
|
817
817
|
|
|
@@ -819,7 +819,7 @@ class ISubscriptionManager:
|
|
|
819
819
|
subscription_type: Type of subscription to filter by (optional)
|
|
820
820
|
|
|
821
821
|
Returns:
|
|
822
|
-
|
|
822
|
+
list[Instrument]: list of subscribed instruments
|
|
823
823
|
"""
|
|
824
824
|
...
|
|
825
825
|
|
|
@@ -963,7 +963,7 @@ class IAccountProcessor(IAccountViewer):
|
|
|
963
963
|
"""
|
|
964
964
|
...
|
|
965
965
|
|
|
966
|
-
def add_active_orders(self, orders:
|
|
966
|
+
def add_active_orders(self, orders: dict[str, Order]) -> None:
|
|
967
967
|
"""Add active orders to the account.
|
|
968
968
|
|
|
969
969
|
Warning only use in the beginning for state restoration because it does not update locked balances.
|
|
@@ -1116,8 +1116,8 @@ class IPositionGathering:
|
|
|
1116
1116
|
def alter_position_size(self, ctx: IStrategyContext, target: TargetPosition) -> float: ...
|
|
1117
1117
|
|
|
1118
1118
|
def alter_positions(
|
|
1119
|
-
self, ctx: IStrategyContext, targets:
|
|
1120
|
-
) ->
|
|
1119
|
+
self, ctx: IStrategyContext, targets: list[TargetPosition] | TargetPosition
|
|
1120
|
+
) -> dict[Instrument, float]:
|
|
1121
1121
|
if not isinstance(targets, list):
|
|
1122
1122
|
targets = [targets]
|
|
1123
1123
|
|
|
@@ -1195,7 +1195,7 @@ class PositionsTracker:
|
|
|
1195
1195
|
|
|
1196
1196
|
def update(
|
|
1197
1197
|
self, ctx: IStrategyContext, instrument: Instrument, update: Timestamped
|
|
1198
|
-
) ->
|
|
1198
|
+
) -> list[TargetPosition] | TargetPosition:
|
|
1199
1199
|
"""
|
|
1200
1200
|
Tracker is being updated by new market data.
|
|
1201
1201
|
It may require to change position size or create new position because of interior tracker's logic (risk management for example).
|
|
@@ -1730,7 +1730,7 @@ class IStrategy(metaclass=Mixable):
|
|
|
1730
1730
|
"""
|
|
1731
1731
|
return None
|
|
1732
1732
|
|
|
1733
|
-
def on_event(self, ctx: IStrategyContext, event: TriggerEvent) ->
|
|
1733
|
+
def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> list[Signal] | Signal | None:
|
|
1734
1734
|
"""Called on strategy events.
|
|
1735
1735
|
|
|
1736
1736
|
Args:
|
|
@@ -1742,7 +1742,7 @@ class IStrategy(metaclass=Mixable):
|
|
|
1742
1742
|
"""
|
|
1743
1743
|
return None
|
|
1744
1744
|
|
|
1745
|
-
def on_market_data(self, ctx: IStrategyContext, data: MarketEvent) ->
|
|
1745
|
+
def on_market_data(self, ctx: IStrategyContext, data: MarketEvent) -> list[Signal] | Signal | None:
|
|
1746
1746
|
"""
|
|
1747
1747
|
Called when new market data is received.
|
|
1748
1748
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
qubx/ta/indicators.pxd
CHANGED
|
@@ -59,7 +59,7 @@ cdef class Lowest(Indicator):
|
|
|
59
59
|
|
|
60
60
|
cdef class Std(Indicator):
|
|
61
61
|
cdef int period
|
|
62
|
-
cdef RollingSum rolling_sum,
|
|
62
|
+
cdef RollingSum rolling_sum, rolling_sum_sq
|
|
63
63
|
cpdef double calculate(self, long long time, double value, short new_item_started)
|
|
64
64
|
|
|
65
65
|
|
qubx/ta/indicators.pyx
CHANGED
|
@@ -226,29 +226,26 @@ cdef class Std(Indicator):
|
|
|
226
226
|
def __init__(self, str name, TimeSeries series, int period):
|
|
227
227
|
self.period = period
|
|
228
228
|
self.rolling_sum = RollingSum(period)
|
|
229
|
-
self.
|
|
229
|
+
self.rolling_sum_sq = RollingSum(period)
|
|
230
230
|
super().__init__(name, series)
|
|
231
231
|
|
|
232
232
|
cpdef double calculate(self, long long time, double value, short new_item_started):
|
|
233
233
|
# Update the rolling sum with the new value
|
|
234
234
|
cdef double _sum = self.rolling_sum.update(value, new_item_started)
|
|
235
|
+
cdef double _sum_sq = self.rolling_sum_sq.update(value * value, new_item_started)
|
|
235
236
|
|
|
236
237
|
# If we're still in the initialization stage, return NaN
|
|
237
|
-
if self.rolling_sum.is_init_stage:
|
|
238
|
+
if self.rolling_sum.is_init_stage or self.rolling_sum_sq.is_init_stage:
|
|
238
239
|
return np.nan
|
|
239
240
|
|
|
240
241
|
# Calculate the mean from the rolling sum
|
|
241
242
|
cdef double _mean = _sum / self.period
|
|
242
243
|
|
|
243
244
|
# Update the variance sum with the squared deviation from the mean
|
|
244
|
-
cdef double
|
|
245
|
-
|
|
246
|
-
# If the variance sum is still in the initialization stage, return NaN
|
|
247
|
-
if self.variance_sum.is_init_stage:
|
|
248
|
-
return np.nan
|
|
245
|
+
cdef double _variance = _sum_sq / self.period - _mean * _mean
|
|
249
246
|
|
|
250
247
|
# Return the square root of the variance (standard deviation)
|
|
251
|
-
return np.sqrt(
|
|
248
|
+
return np.sqrt(_variance)
|
|
252
249
|
|
|
253
250
|
|
|
254
251
|
def std(series: TimeSeries, period: int, mean=0):
|
qubx/utils/runner/runner.py
CHANGED
|
@@ -13,6 +13,7 @@ from qubx.backtester.account import SimulatedAccountProcessor
|
|
|
13
13
|
from qubx.backtester.broker import SimulatedBroker
|
|
14
14
|
from qubx.backtester.optimization import variate
|
|
15
15
|
from qubx.backtester.runner import SimulationRunner
|
|
16
|
+
from qubx.backtester.simulated_exchange import get_simulated_exchange
|
|
16
17
|
from qubx.backtester.utils import (
|
|
17
18
|
SetupTypes,
|
|
18
19
|
SimulatedLogFormatter,
|
|
@@ -452,12 +453,15 @@ def _create_account_processor(
|
|
|
452
453
|
) -> IAccountProcessor:
|
|
453
454
|
if paper:
|
|
454
455
|
settings = account_manager.get_exchange_settings(exchange_name)
|
|
456
|
+
|
|
457
|
+
# - TODO: here we can create different types of simulated exchanges based on it's name etc
|
|
458
|
+
simulated_exchange = get_simulated_exchange(exchange_name, time_provider, tcc)
|
|
459
|
+
|
|
455
460
|
return SimulatedAccountProcessor(
|
|
456
461
|
account_id=exchange_name,
|
|
462
|
+
exchange=simulated_exchange,
|
|
457
463
|
channel=channel,
|
|
458
464
|
base_currency=settings.base_currency,
|
|
459
|
-
time_provider=time_provider,
|
|
460
|
-
tcc=tcc,
|
|
461
465
|
initial_capital=settings.initial_capital,
|
|
462
466
|
restored_state=restored_state,
|
|
463
467
|
)
|
|
@@ -493,7 +497,7 @@ def _create_broker(
|
|
|
493
497
|
) -> IBroker:
|
|
494
498
|
if paper:
|
|
495
499
|
assert isinstance(account, SimulatedAccountProcessor)
|
|
496
|
-
return SimulatedBroker(channel=channel, account=account,
|
|
500
|
+
return SimulatedBroker(channel=channel, account=account, simulated_exchange=account._exchange)
|
|
497
501
|
|
|
498
502
|
creds = account_manager.get_exchange_credentials(exchange_name)
|
|
499
503
|
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
qubx/__init__.py,sha256=GBvbyDpm2yCMJVmGW66Jo0giLOUsKKldDGcVA_r9Ohc,8294
|
|
2
2
|
qubx/_nb_magic.py,sha256=kcYn8qNb8O223ZRPpq30_n5e__lD5GSVcd0U_jhfnbM,3019
|
|
3
3
|
qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
|
|
4
|
-
qubx/backtester/account.py,sha256=
|
|
5
|
-
qubx/backtester/broker.py,sha256=
|
|
6
|
-
qubx/backtester/data.py,sha256=
|
|
4
|
+
qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,3048
|
|
5
|
+
qubx/backtester/broker.py,sha256=JMasxycLqCT99NxN50uyQ1uxtpHYL0wpp4sJ3hB6v2M,2688
|
|
6
|
+
qubx/backtester/data.py,sha256=vxpZCoFZsxI7QKDfragGlb0-tCplMJH99xVZWKcvEm0,11981
|
|
7
7
|
qubx/backtester/management.py,sha256=HuyzFsBPgR7j-ei78Ngcx34CeSn65c9atmaii1aTsYg,14900
|
|
8
|
-
qubx/backtester/ome.py,sha256=
|
|
8
|
+
qubx/backtester/ome.py,sha256=hfqwvaMBccDpy6eMe04QupEA6BjfjAodbm707fbQUb0,14984
|
|
9
9
|
qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
|
|
10
|
-
qubx/backtester/runner.py,sha256=
|
|
10
|
+
qubx/backtester/runner.py,sha256=VM0qiq27lccBDNjenYrjIJD3acMtTNfs3T9HJCBcnLk,11507
|
|
11
11
|
qubx/backtester/simulated_data.py,sha256=niujaMRj__jf4IyzCZrSBR5ZoH1VUbvsZHSewHftdmI,17240
|
|
12
|
+
qubx/backtester/simulated_exchange.py,sha256=ATGcJXnKdD47kUwgbc5tvPVL0tq4_-6jpgsTTAMxW3c,8124
|
|
12
13
|
qubx/backtester/simulator.py,sha256=JkGm5xP8q0p4BuR_PEoDm66Uaqc0u-NX-vyjkia_0Vc,9410
|
|
13
14
|
qubx/backtester/utils.py,sha256=6WKTlw6nArnZgGrJatDl8ExqvTHH0SyqEbDbMgiJbIk,33539
|
|
14
15
|
qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -30,13 +31,13 @@ qubx/connectors/ccxt/utils.py,sha256=oMvL0Zh-Tt9pKPTCcZMTo293mScvTiZlE9XKw2-qDgg
|
|
|
30
31
|
qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
32
|
qubx/core/account.py,sha256=b_tKnXQxpQCoJB-kVvrFzzTgDz2lLcebQdtP99x9N8I,10912
|
|
32
33
|
qubx/core/basics.py,sha256=fynmFt6n8dc-9g9IuaXxixB8x52VQ1yFQDDK_FH3LmU,29463
|
|
33
|
-
qubx/core/context.py,sha256=
|
|
34
|
+
qubx/core/context.py,sha256=PmO0x5VWGXY6zMF7H2rrTgPwNhDhq-f5Y8diUoZmNE0,21875
|
|
34
35
|
qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
|
|
35
36
|
qubx/core/errors.py,sha256=kWCK6o0-mm87VUhhlGKqwTpvdDXAza7YRRjeyz-vwfI,609
|
|
36
37
|
qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
|
|
37
38
|
qubx/core/helpers.py,sha256=9nl9L_ZzT1HsMC9VthMqXfmuRS_37crB-9bVfIRHeOs,19631
|
|
38
39
|
qubx/core/initializer.py,sha256=PUiD_cIjvGpuPjYyRpUjpwm3xNQ2Kipa8bAhbtxCQRo,3935
|
|
39
|
-
qubx/core/interfaces.py,sha256=
|
|
40
|
+
qubx/core/interfaces.py,sha256=sUs1oIcLI3ZZL78Cl9IewTpEtAERfuAiZ57xrP4HNrs,57444
|
|
40
41
|
qubx/core/loggers.py,sha256=eYhJANHYwz1heeFMa5V7jYCL196wkTSvj6c-8lkPj1Y,19567
|
|
41
42
|
qubx/core/lookups.py,sha256=KBE0ab4eheA6C5C-RIND_svYhE7Glb4CSlLRjMhPNRc,15906
|
|
42
43
|
qubx/core/metrics.py,sha256=2AocZUYwoZE8OvScN3ULa-wyVregzlCn722QIZUe7Q8,57784
|
|
@@ -46,11 +47,11 @@ qubx/core/mixins/processing.py,sha256=dqehukrfqcLy5BeILKnkpHCvva4SbLKj1ZbQdnByu1
|
|
|
46
47
|
qubx/core/mixins/subscription.py,sha256=J_SX0CNw2bPy4bhxe0vswvDXY4LCkwXSaj_1PepKRLY,8540
|
|
47
48
|
qubx/core/mixins/trading.py,sha256=wqG-o7PxUZLd_yUAg0MUbq2LfnXmTcBLETrGv8vs9jU,6819
|
|
48
49
|
qubx/core/mixins/universe.py,sha256=1oqOadbVB9nlvQQp2jDRhUvsSQkyxdoNgswxnxHTcs4,10653
|
|
49
|
-
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
50
|
+
qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=GK9DRBxZYO3ReW8861L8LC39bjk54BKNAAlCrr1KiOU,978280
|
|
50
51
|
qubx/core/series.pxd,sha256=jBdMwgO8J4Zrue0e_xQ5RlqTXqihpzQNu6V3ckZvvpY,3978
|
|
51
52
|
qubx/core/series.pyi,sha256=RaHm_oHHiWiNUMJqVfx5FXAXniGLsHxUFOUpacn7GC0,4604
|
|
52
53
|
qubx/core/series.pyx,sha256=7DTYRj5D6O_O5MoDBj_FBjQh2zdxofi3912AnlZ7BiY,46477
|
|
53
|
-
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
54
|
+
qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=Wgpzupp_ZIIRrY4m_GRoIpFXfb8LQxlOXsUIvcSjLeU,86568
|
|
54
55
|
qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
|
|
55
56
|
qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
|
|
56
57
|
qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
|
|
@@ -111,10 +112,10 @@ qubx/restorers/signal.py,sha256=DBLqA7vDhoMTAzUC4N9UerrO0GbjeHdTeMoCz7U7iI8,6621
|
|
|
111
112
|
qubx/restorers/state.py,sha256=ePmh604fp2kRYzMNXL-TWvZOxmtTGAaKYfHJcnKResY,4042
|
|
112
113
|
qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
|
|
113
114
|
qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
114
|
-
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=
|
|
115
|
-
qubx/ta/indicators.pxd,sha256=
|
|
115
|
+
qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=qmqUiyajgSh8E1aWvOY32KMbVxOhHDhkcFSUjtx5Evo,654440
|
|
116
|
+
qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
|
|
116
117
|
qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
|
|
117
|
-
qubx/ta/indicators.pyx,sha256=
|
|
118
|
+
qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
|
|
118
119
|
qubx/trackers/__init__.py,sha256=ThIP1jXaACse5hG3lZqQSlWSKYl6APxFmBHaRcVpPdU,1002
|
|
119
120
|
qubx/trackers/advanced.py,sha256=WiYqihr5ZIY74DjiCzyMCywB7F1wTt71qrXqlzzIbZQ,12764
|
|
120
121
|
qubx/trackers/composite.py,sha256=W-n1vd4l-RZEoojj6lICqvJ8EgTV2kE6JUUmZUkZ1cI,6339
|
|
@@ -144,11 +145,11 @@ qubx/utils/runner/_jupyter_runner.pyt,sha256=41dLQeI2EL4wJjBDht2qKbgISgS5DtmJ6Xt
|
|
|
144
145
|
qubx/utils/runner/accounts.py,sha256=brf_RP22-nFlpyLg-eES-4pCikqev-I7OHa2bnLDK6o,3304
|
|
145
146
|
qubx/utils/runner/configs.py,sha256=Sr4h-1d84Im29_nNtHu_UCQh1-MJGudU9MvIZmh2sWA,3462
|
|
146
147
|
qubx/utils/runner/factory.py,sha256=vQ2dBTbrQE9YH__-TvuFzGF-E1li-vt_qQum9GHa11g,11666
|
|
147
|
-
qubx/utils/runner/runner.py,sha256=
|
|
148
|
+
qubx/utils/runner/runner.py,sha256=03w1pZdbiXuGdcUbPy3_HLUoCk20IzmAL58FSXCYMNE,28216
|
|
148
149
|
qubx/utils/time.py,sha256=J0ZFGjzFL5T6GA8RPAel8hKG0sg2LZXeQ5YfDCfcMHA,10055
|
|
149
150
|
qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
|
|
150
|
-
qubx-0.6.
|
|
151
|
-
qubx-0.6.
|
|
152
|
-
qubx-0.6.
|
|
153
|
-
qubx-0.6.
|
|
154
|
-
qubx-0.6.
|
|
151
|
+
qubx-0.6.23.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
|
|
152
|
+
qubx-0.6.23.dist-info/METADATA,sha256=w1vDSRGZxYqvAKCoJUlT3605G_03Y4IHsMjbPafcRyk,4444
|
|
153
|
+
qubx-0.6.23.dist-info/WHEEL,sha256=XjdW4AGUgFDhpG9b3b2KPhtR_JLZvHyfemLgJJwcqOI,110
|
|
154
|
+
qubx-0.6.23.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
|
|
155
|
+
qubx-0.6.23.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|