Qubx 0.6.21__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.24__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 +8 -138
- qubx/backtester/ome.py +13 -9
- qubx/backtester/runner.py +256 -40
- qubx/backtester/simulated_exchange.py +233 -0
- qubx/backtester/simulator.py +13 -22
- qubx/backtester/utils.py +27 -39
- qubx/connectors/ccxt/account.py +4 -4
- qubx/connectors/ccxt/data.py +93 -18
- qubx/connectors/ccxt/exchanges/__init__.py +5 -1
- qubx/connectors/ccxt/exchanges/binance/exchange.py +1 -0
- qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +43 -0
- qubx/connectors/ccxt/exchanges/kraken/kraken.py +14 -0
- qubx/connectors/ccxt/utils.py +20 -6
- qubx/connectors/tardis/data.py +733 -0
- qubx/connectors/tardis/utils.py +249 -0
- qubx/core/account.py +206 -20
- qubx/core/basics.py +0 -9
- qubx/core/context.py +56 -53
- qubx/core/interfaces.py +67 -65
- qubx/core/lookups.py +129 -18
- qubx/core/metrics.py +14 -11
- qubx/core/mixins/market.py +24 -9
- qubx/core/mixins/subscription.py +58 -28
- qubx/core/mixins/trading.py +35 -31
- qubx/core/mixins/universe.py +0 -20
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pyx +1 -1
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/data/helpers.py +1 -1
- qubx/data/tardis.py +0 -1
- qubx/restorers/state.py +2 -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/accounts.py +0 -1
- qubx/utils/runner/configs.py +8 -0
- qubx/utils/runner/runner.py +43 -18
- {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/METADATA +1 -1
- {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/RECORD +44 -39
- {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/WHEEL +1 -1
- {qubx-0.6.21.dist-info → qubx-0.6.24.dist-info}/LICENSE +0 -0
- {qubx-0.6.21.dist-info → qubx-0.6.24.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,9 +1,6 @@
|
|
|
1
1
|
from collections import defaultdict
|
|
2
|
-
from typing import Any, Dict, Optional
|
|
3
2
|
|
|
4
|
-
import numpy as np
|
|
5
3
|
import pandas as pd
|
|
6
|
-
from tqdm.auto import tqdm
|
|
7
4
|
|
|
8
5
|
from qubx import logger
|
|
9
6
|
from qubx.backtester.simulated_data import IterableSimulationData
|
|
@@ -13,7 +10,6 @@ from qubx.core.basics import (
|
|
|
13
10
|
Instrument,
|
|
14
11
|
TimestampedDict,
|
|
15
12
|
)
|
|
16
|
-
from qubx.core.exceptions import SimulationError
|
|
17
13
|
from qubx.core.helpers import BasicScheduler
|
|
18
14
|
from qubx.core.interfaces import IDataProvider
|
|
19
15
|
from qubx.core.series import Bar, Quote, time_as_nsec
|
|
@@ -30,11 +26,8 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
30
26
|
|
|
31
27
|
_scheduler: BasicScheduler
|
|
32
28
|
_account: SimulatedAccountProcessor
|
|
33
|
-
_last_quotes:
|
|
29
|
+
_last_quotes: dict[Instrument, Quote | None]
|
|
34
30
|
_readers: dict[str, DataReader]
|
|
35
|
-
_scheduler: BasicScheduler
|
|
36
|
-
_pregenerated_signals: dict[Instrument, pd.Series | pd.DataFrame]
|
|
37
|
-
_to_process: dict[Instrument, list]
|
|
38
31
|
_data_source: IterableSimulationData
|
|
39
32
|
_open_close_time_indent_ns: int
|
|
40
33
|
|
|
@@ -46,6 +39,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
46
39
|
time_provider: SimulatedTimeProvider,
|
|
47
40
|
account: SimulatedAccountProcessor,
|
|
48
41
|
readers: dict[str, DataReader],
|
|
42
|
+
data_source: IterableSimulationData,
|
|
49
43
|
open_close_time_indent_secs=1,
|
|
50
44
|
):
|
|
51
45
|
self.channel = channel
|
|
@@ -55,79 +49,14 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
55
49
|
self._account = account
|
|
56
50
|
self._readers = readers
|
|
57
51
|
|
|
58
|
-
# - create exchange's instance
|
|
59
|
-
self._last_quotes = defaultdict(lambda: None)
|
|
60
|
-
|
|
61
|
-
# - pregenerated signals storage
|
|
62
|
-
self._pregenerated_signals = dict()
|
|
63
|
-
self._to_process = {}
|
|
64
|
-
|
|
65
52
|
# - simulation data source
|
|
66
|
-
self._data_source =
|
|
67
|
-
self._readers, open_close_time_indent_secs=open_close_time_indent_secs
|
|
68
|
-
)
|
|
53
|
+
self._data_source = data_source
|
|
69
54
|
self._open_close_time_indent_ns = open_close_time_indent_secs * 1_000_000_000 # convert seconds to nanoseconds
|
|
70
55
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def run(
|
|
74
|
-
self,
|
|
75
|
-
start: str | pd.Timestamp,
|
|
76
|
-
end: str | pd.Timestamp,
|
|
77
|
-
silent: bool = False,
|
|
78
|
-
) -> None:
|
|
79
|
-
logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
|
|
80
|
-
|
|
81
|
-
if self._pregenerated_signals:
|
|
82
|
-
self._prepare_generated_signals(start, end)
|
|
83
|
-
_run = self._process_generated_signals
|
|
84
|
-
else:
|
|
85
|
-
_run = self._process_strategy
|
|
86
|
-
|
|
87
|
-
start, end = pd.Timestamp(start), pd.Timestamp(end)
|
|
88
|
-
total_duration = end - start
|
|
89
|
-
update_delta = total_duration / 100
|
|
90
|
-
prev_dt = pd.Timestamp(start)
|
|
91
|
-
|
|
92
|
-
# - date iteration
|
|
93
|
-
qiter = self._data_source.create_iterable(start, end)
|
|
94
|
-
if silent:
|
|
95
|
-
for instrument, data_type, event, is_hist in qiter:
|
|
96
|
-
if not _run(instrument, data_type, event, is_hist):
|
|
97
|
-
break
|
|
98
|
-
else:
|
|
99
|
-
_p = 0
|
|
100
|
-
with tqdm(total=100, desc="Simulating", unit="%", leave=False) as pbar:
|
|
101
|
-
for instrument, data_type, event, is_hist in qiter:
|
|
102
|
-
if not _run(instrument, data_type, event, is_hist):
|
|
103
|
-
break
|
|
104
|
-
dt = pd.Timestamp(event.time)
|
|
105
|
-
# update only if date has changed
|
|
106
|
-
if dt - prev_dt > update_delta:
|
|
107
|
-
_p += 1
|
|
108
|
-
pbar.n = _p
|
|
109
|
-
pbar.refresh()
|
|
110
|
-
prev_dt = dt
|
|
111
|
-
pbar.n = 100
|
|
112
|
-
pbar.refresh()
|
|
113
|
-
|
|
114
|
-
logger.info(f"{self.__class__.__name__} ::: Simulation finished at {end} :::")
|
|
115
|
-
|
|
116
|
-
def set_generated_signals(self, signals: pd.Series | pd.DataFrame):
|
|
117
|
-
logger.debug(
|
|
118
|
-
f"[<y>{self.__class__.__name__}</y>] :: Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}"
|
|
119
|
-
)
|
|
120
|
-
# - sanity check
|
|
121
|
-
signals.index = pd.DatetimeIndex(signals.index)
|
|
122
|
-
|
|
123
|
-
if isinstance(signals, pd.Series):
|
|
124
|
-
self._pregenerated_signals[str(signals.name)] = signals # type: ignore
|
|
56
|
+
# - create exchange's instance
|
|
57
|
+
self._last_quotes = defaultdict(lambda: None)
|
|
125
58
|
|
|
126
|
-
|
|
127
|
-
for col in signals.columns:
|
|
128
|
-
self._pregenerated_signals[col] = signals[col] # type: ignore
|
|
129
|
-
else:
|
|
130
|
-
raise ValueError("Invalid signals or strategy configuration")
|
|
59
|
+
logger.info(f"{self.__class__.__name__}.{exchange_id} is initialized")
|
|
131
60
|
|
|
132
61
|
@property
|
|
133
62
|
def is_simulation(self) -> bool:
|
|
@@ -143,7 +72,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
143
72
|
if h_data:
|
|
144
73
|
# _s_type = DataType.from_str(subscription_type)[0]
|
|
145
74
|
last_update = h_data[-1]
|
|
146
|
-
if last_quote := self._account.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
|
|
75
|
+
if last_quote := self._account._exchange.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
|
|
147
76
|
# - send historical data to the channel
|
|
148
77
|
self.channel.send((i, subscription_type, h_data, True))
|
|
149
78
|
|
|
@@ -151,7 +80,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
151
80
|
self._last_quotes[i] = last_quote
|
|
152
81
|
|
|
153
82
|
# - also need to pass this quote to OME !
|
|
154
|
-
self._account.
|
|
83
|
+
self._account.process_market_data(last_quote.time, i, last_quote) # type: ignore
|
|
155
84
|
|
|
156
85
|
logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
|
|
157
86
|
|
|
@@ -201,26 +130,6 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
201
130
|
def close(self):
|
|
202
131
|
pass
|
|
203
132
|
|
|
204
|
-
def _prepare_generated_signals(self, start: str | pd.Timestamp, end: str | pd.Timestamp):
|
|
205
|
-
for s, v in self._pregenerated_signals.items():
|
|
206
|
-
_s_inst = None
|
|
207
|
-
|
|
208
|
-
for i in self.get_subscribed_instruments():
|
|
209
|
-
# - we can process series with variable id's if we can find some similar instrument
|
|
210
|
-
if s == i.symbol or s == str(i) or s == f"{i.exchange}:{i.symbol}" or str(s) == str(i):
|
|
211
|
-
_start, _end = pd.Timestamp(start), pd.Timestamp(end)
|
|
212
|
-
_start_idx, _end_idx = v.index.get_indexer([_start, _end], method="ffill")
|
|
213
|
-
sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
|
|
214
|
-
|
|
215
|
-
# TODO: check if data has exec_price - it means we have deals
|
|
216
|
-
self._to_process[i] = list(zip(sel.index, sel.values))
|
|
217
|
-
_s_inst = i
|
|
218
|
-
break
|
|
219
|
-
|
|
220
|
-
if _s_inst is None:
|
|
221
|
-
logger.error(f"Can't find instrument for pregenerated signals with id '{s}'")
|
|
222
|
-
raise SimulationError(f"Can't find instrument for pregenerated signals with id '{s}'")
|
|
223
|
-
|
|
224
133
|
def _convert_records_to_bars(
|
|
225
134
|
self, records: list[TimestampedDict], cut_time_ns: int, timeframe_ns: int
|
|
226
135
|
) -> list[Bar]:
|
|
@@ -253,44 +162,5 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
253
162
|
|
|
254
163
|
return bars
|
|
255
164
|
|
|
256
|
-
def _process_generated_signals(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
|
|
257
|
-
cc = self.channel
|
|
258
|
-
t = np.datetime64(data.time, "ns")
|
|
259
|
-
|
|
260
|
-
if not is_hist:
|
|
261
|
-
# - signals for this instrument
|
|
262
|
-
sigs = self._to_process[instrument]
|
|
263
|
-
|
|
264
|
-
while sigs and t >= (_signal_time := sigs[0][0].as_unit("ns").asm8):
|
|
265
|
-
self.time_provider.set_time(_signal_time)
|
|
266
|
-
cc.send((instrument, "event", {"order": sigs[0][1]}, False))
|
|
267
|
-
sigs.pop(0)
|
|
268
|
-
|
|
269
|
-
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
270
|
-
self._last_quotes[instrument] = q
|
|
271
|
-
|
|
272
|
-
self.time_provider.set_time(t)
|
|
273
|
-
cc.send((instrument, data_type, data, is_hist))
|
|
274
|
-
|
|
275
|
-
return cc.control.is_set()
|
|
276
|
-
|
|
277
|
-
def _process_strategy(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
|
|
278
|
-
cc = self.channel
|
|
279
|
-
t = np.datetime64(data.time, "ns")
|
|
280
|
-
|
|
281
|
-
if not is_hist:
|
|
282
|
-
if t >= (_next_exp_time := self._scheduler.next_expected_event_time()):
|
|
283
|
-
# - we use exact event's time
|
|
284
|
-
self.time_provider.set_time(_next_exp_time)
|
|
285
|
-
self._scheduler.check_and_run_tasks()
|
|
286
|
-
|
|
287
|
-
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
288
|
-
self._last_quotes[instrument] = q
|
|
289
|
-
|
|
290
|
-
self.time_provider.set_time(t)
|
|
291
|
-
cc.send((instrument, data_type, data, is_hist))
|
|
292
|
-
|
|
293
|
-
return cc.control.is_set()
|
|
294
|
-
|
|
295
165
|
def exchange(self) -> str:
|
|
296
166
|
return self._exchange_id.upper()
|
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
|