Qubx 0.5.0__cp311-cp311-manylinux_2_35_x86_64.whl → 0.5.1__cp311-cp311-manylinux_2_35_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/__init__.py +10 -6
- qubx/_nb_magic.py +23 -23
- qubx/backtester/account.py +14 -2
- qubx/backtester/broker.py +5 -0
- qubx/backtester/data.py +3 -0
- qubx/backtester/ome.py +5 -8
- qubx/backtester/optimization.py +5 -5
- qubx/backtester/simulated_data.py +12 -13
- qubx/backtester/simulator.py +5 -3
- qubx/connectors/ccxt/broker.py +10 -4
- qubx/connectors/ccxt/data.py +11 -0
- qubx/core/basics.py +38 -115
- qubx/core/context.py +8 -1
- qubx/core/helpers.py +5 -3
- qubx/core/interfaces.py +35 -2
- qubx/core/loggers.py +1 -0
- qubx/core/metrics.py +248 -13
- qubx/core/mixins/processing.py +45 -53
- qubx/core/mixins/subscription.py +7 -4
- qubx/core/mixins/trading.py +3 -0
- qubx/core/series.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/data/helpers.py +1 -1
- qubx/data/readers.py +8 -4
- qubx/pandaz/utils.py +3 -0
- qubx/ta/indicators.cpython-311-x86_64-linux-gnu.so +0 -0
- qubx/trackers/composite.py +5 -5
- qubx/trackers/rebalancers.py +13 -27
- qubx/trackers/riskctrl.py +10 -9
- qubx/trackers/sizers.py +4 -7
- qubx/utils/_jupyter_runner.pyt +59 -0
- qubx/utils/misc.py +8 -0
- qubx/utils/runner.py +198 -140
- {qubx-0.5.0.dist-info → qubx-0.5.1.dist-info}/METADATA +3 -1
- {qubx-0.5.0.dist-info → qubx-0.5.1.dist-info}/RECORD +37 -36
- qubx-0.5.1.dist-info/entry_points.txt +3 -0
- qubx/utils/helpers.py +0 -14
- {qubx-0.5.0.dist-info → qubx-0.5.1.dist-info}/WHEEL +0 -0
qubx/__init__.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
1
3
|
from typing import Callable
|
|
2
|
-
from qubx.utils import set_mpl_theme, runtime_env
|
|
3
|
-
from qubx.utils.misc import install_pyx_recompiler_for_dev
|
|
4
4
|
|
|
5
|
+
import stackprinter
|
|
5
6
|
from loguru import logger
|
|
6
|
-
|
|
7
|
+
|
|
7
8
|
from qubx.core.lookups import FeesLookup, GlobalLookup, InstrumentsLookup
|
|
9
|
+
from qubx.utils import runtime_env, set_mpl_theme
|
|
10
|
+
from qubx.utils.misc import install_pyx_recompiler_for_dev
|
|
8
11
|
|
|
9
12
|
# - TODO: import some main methods from packages
|
|
10
13
|
|
|
@@ -66,8 +69,8 @@ lookup = GlobalLookup(InstrumentsLookup(), FeesLookup())
|
|
|
66
69
|
|
|
67
70
|
# registering magic for jupyter notebook
|
|
68
71
|
if runtime_env() in ["notebook", "shell"]:
|
|
69
|
-
from IPython.core.magic import Magics, magics_class, line_magic, line_cell_magic
|
|
70
72
|
from IPython.core.getipython import get_ipython
|
|
73
|
+
from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
|
|
71
74
|
|
|
72
75
|
@magics_class
|
|
73
76
|
class QubxMagics(Magics):
|
|
@@ -136,7 +139,8 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
136
139
|
|
|
137
140
|
"""
|
|
138
141
|
import multiprocessing as m
|
|
139
|
-
import
|
|
142
|
+
import re
|
|
143
|
+
import time
|
|
140
144
|
|
|
141
145
|
# create ext args
|
|
142
146
|
name = None
|
|
@@ -151,7 +155,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
151
155
|
return
|
|
152
156
|
|
|
153
157
|
ipy = get_ipython()
|
|
154
|
-
for a in [x for x in re.split("[\ ,;]", line.strip()) if x]:
|
|
158
|
+
for a in [x for x in re.split(r"[\ ,;]", line.strip()) if x]:
|
|
155
159
|
ipy.push({a: self._get_manager().Value(None, None)})
|
|
156
160
|
|
|
157
161
|
# code to run
|
qubx/_nb_magic.py
CHANGED
|
@@ -32,60 +32,60 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
32
32
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
33
33
|
|
|
34
34
|
# - - - - Common stuff - - - -
|
|
35
|
+
from datetime import time, timedelta
|
|
36
|
+
|
|
35
37
|
import numpy as np
|
|
36
38
|
import pandas as pd
|
|
37
|
-
|
|
39
|
+
|
|
40
|
+
# - - - - Charting stuff - - - -
|
|
41
|
+
from matplotlib import pyplot as plt
|
|
38
42
|
from tqdm.auto import tqdm
|
|
39
43
|
|
|
40
44
|
# - - - - TA stuff and indicators - - - -
|
|
41
45
|
import qubx.pandaz.ta as pta
|
|
42
46
|
import qubx.ta.indicators as ta
|
|
47
|
+
from qubx.backtester.optimization import variate
|
|
48
|
+
|
|
49
|
+
# - - - - Simulator stuff - - - -
|
|
50
|
+
from qubx.backtester.simulator import simulate
|
|
43
51
|
|
|
44
52
|
# - - - - Portfolio analysis - - - -
|
|
45
53
|
from qubx.core.metrics import (
|
|
46
|
-
tearsheet,
|
|
47
54
|
chart_signals,
|
|
48
|
-
get_symbol_pnls,
|
|
49
|
-
get_equity,
|
|
50
|
-
portfolio_metrics,
|
|
51
|
-
pnl,
|
|
52
55
|
drop_symbols,
|
|
56
|
+
get_symbol_pnls,
|
|
53
57
|
pick_symbols,
|
|
58
|
+
pnl,
|
|
59
|
+
portfolio_metrics,
|
|
60
|
+
tearsheet,
|
|
54
61
|
)
|
|
62
|
+
from qubx.data.helpers import loader
|
|
55
63
|
|
|
56
64
|
# - - - - Data reading - - - -
|
|
57
65
|
from qubx.data.readers import (
|
|
58
|
-
CsvStorageDataReader,
|
|
59
|
-
MultiQdbConnector,
|
|
60
|
-
QuestDBConnector,
|
|
61
66
|
AsOhlcvSeries,
|
|
62
67
|
AsPandasFrame,
|
|
63
68
|
AsQuotes,
|
|
64
69
|
AsTimestampedRecords,
|
|
70
|
+
CsvStorageDataReader,
|
|
71
|
+
MultiQdbConnector,
|
|
72
|
+
QuestDBConnector,
|
|
65
73
|
RestoreTicksFromOHLC,
|
|
66
74
|
)
|
|
67
|
-
from qubx.data.helpers import loader
|
|
68
|
-
|
|
69
|
-
# - - - - Simulator stuff - - - -
|
|
70
|
-
from qubx.backtester.simulator import simulate
|
|
71
|
-
from qubx.backtester.optimization import variate
|
|
72
|
-
|
|
73
|
-
# - - - - Charting stuff - - - -
|
|
74
|
-
from matplotlib import pyplot as plt
|
|
75
|
-
from qubx.utils.charting.mpl_helpers import fig, subplot, sbp, plot_trends, ohlc_plot
|
|
76
|
-
from qubx.utils.charting.lookinglass import LookingGlass
|
|
77
75
|
|
|
78
76
|
# - - - - Utils - - - -
|
|
79
77
|
from qubx.pandaz.utils import (
|
|
80
|
-
scols,
|
|
81
|
-
srows,
|
|
82
|
-
ohlc_resample,
|
|
83
78
|
continuous_periods,
|
|
84
|
-
generate_equal_date_ranges,
|
|
85
79
|
drop_duplicated_indexes,
|
|
80
|
+
generate_equal_date_ranges,
|
|
81
|
+
ohlc_resample,
|
|
86
82
|
retain_columns_and_join,
|
|
87
83
|
rolling_forward_test_split,
|
|
84
|
+
scols,
|
|
85
|
+
srows,
|
|
88
86
|
)
|
|
87
|
+
from qubx.utils.charting.lookinglass import LookingGlass
|
|
88
|
+
from qubx.utils.charting.mpl_helpers import fig, ohlc_plot, plot_trends, sbp, subplot
|
|
89
89
|
|
|
90
90
|
# - setup short numpy output format
|
|
91
91
|
np_fmt_short()
|
qubx/backtester/account.py
CHANGED
|
@@ -3,15 +3,17 @@ from qubx.backtester.ome import OrdersManagementEngine
|
|
|
3
3
|
from qubx.core.account import BasicAccountProcessor
|
|
4
4
|
from qubx.core.basics import (
|
|
5
5
|
ZERO_COSTS,
|
|
6
|
+
BatchEvent,
|
|
6
7
|
CtrlChannel,
|
|
7
8
|
Instrument,
|
|
8
9
|
Order,
|
|
9
10
|
Position,
|
|
11
|
+
Timestamped,
|
|
10
12
|
TransactionCostsCalculator,
|
|
11
13
|
dt_64,
|
|
12
14
|
)
|
|
13
15
|
from qubx.core.interfaces import ITimeProvider
|
|
14
|
-
from qubx.core.series import Bar, Quote, Trade
|
|
16
|
+
from qubx.core.series import Bar, OrderBook, Quote, Trade
|
|
15
17
|
|
|
16
18
|
|
|
17
19
|
class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
@@ -101,7 +103,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
101
103
|
return super().process_order(order, update_locked_value)
|
|
102
104
|
|
|
103
105
|
def emulate_quote_from_data(
|
|
104
|
-
self, instrument: Instrument, timestamp: dt_64, data: float |
|
|
106
|
+
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped | BatchEvent
|
|
105
107
|
) -> Quote | None:
|
|
106
108
|
if instrument not in self._half_tick_size:
|
|
107
109
|
_ = self.get_position(instrument)
|
|
@@ -109,15 +111,25 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
109
111
|
_ts2 = self._half_tick_size[instrument]
|
|
110
112
|
if isinstance(data, Quote):
|
|
111
113
|
return data
|
|
114
|
+
|
|
112
115
|
elif isinstance(data, Trade):
|
|
113
116
|
if data.taker: # type: ignore
|
|
114
117
|
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
115
118
|
else:
|
|
116
119
|
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
120
|
+
|
|
117
121
|
elif isinstance(data, Bar):
|
|
118
122
|
return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
|
|
123
|
+
|
|
124
|
+
elif isinstance(data, OrderBook):
|
|
125
|
+
return data.to_quote()
|
|
126
|
+
|
|
127
|
+
elif isinstance(data, BatchEvent):
|
|
128
|
+
return self.emulate_quote_from_data(instrument, timestamp, data.data[-1])
|
|
129
|
+
|
|
119
130
|
elif isinstance(data, float):
|
|
120
131
|
return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
|
|
132
|
+
|
|
121
133
|
else:
|
|
122
134
|
return None
|
|
123
135
|
|
qubx/backtester/broker.py
CHANGED
|
@@ -18,9 +18,11 @@ class SimulatedBroker(IBroker):
|
|
|
18
18
|
self,
|
|
19
19
|
channel: CtrlChannel,
|
|
20
20
|
account: SimulatedAccountProcessor,
|
|
21
|
+
exchange_id: str = "simulated",
|
|
21
22
|
) -> None:
|
|
22
23
|
self.channel = channel
|
|
23
24
|
self._account = account
|
|
25
|
+
self._exchange_id = exchange_id
|
|
24
26
|
|
|
25
27
|
@property
|
|
26
28
|
def is_simulated_trading(self) -> bool:
|
|
@@ -80,3 +82,6 @@ class SimulatedBroker(IBroker):
|
|
|
80
82
|
self.channel.send((instrument, "order", report.order, False))
|
|
81
83
|
if report.exec is not None:
|
|
82
84
|
self.channel.send((instrument, "deals", [report.exec], False))
|
|
85
|
+
|
|
86
|
+
def exchange(self) -> str:
|
|
87
|
+
return self._exchange_id.upper()
|
qubx/backtester/data.py
CHANGED
qubx/backtester/ome.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
from typing import List, Dict
|
|
2
1
|
from dataclasses import dataclass
|
|
3
2
|
from operator import neg
|
|
3
|
+
from typing import Dict, List
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
from sortedcontainers import SortedDict
|
|
7
7
|
|
|
8
8
|
from qubx import logger
|
|
9
9
|
from qubx.core.basics import (
|
|
10
|
+
OPTION_FILL_AT_SIGNAL_PRICE,
|
|
10
11
|
Deal,
|
|
11
12
|
Instrument,
|
|
13
|
+
ITimeProvider,
|
|
12
14
|
Order,
|
|
13
15
|
OrderSide,
|
|
14
16
|
OrderType,
|
|
@@ -16,14 +18,12 @@ from qubx.core.basics import (
|
|
|
16
18
|
Signal,
|
|
17
19
|
TransactionCostsCalculator,
|
|
18
20
|
dt_64,
|
|
19
|
-
ITimeProvider,
|
|
20
|
-
OPTION_FILL_AT_SIGNAL_PRICE,
|
|
21
21
|
)
|
|
22
|
-
from qubx.core.series import Quote, Trade
|
|
23
22
|
from qubx.core.exceptions import (
|
|
24
23
|
ExchangeError,
|
|
25
24
|
InvalidOrder,
|
|
26
25
|
)
|
|
26
|
+
from qubx.core.series import Quote, Trade
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
@@ -127,11 +127,8 @@ class OrdersManagementEngine:
|
|
|
127
127
|
time_in_force: str = "gtc",
|
|
128
128
|
**options,
|
|
129
129
|
) -> OmeReport:
|
|
130
|
-
|
|
131
130
|
if self.bbo is None:
|
|
132
|
-
raise ExchangeError(
|
|
133
|
-
f"Simulator is not ready for order management - no any quote for {self.instrument.symbol}"
|
|
134
|
-
)
|
|
131
|
+
raise ExchangeError(f"Simulator is not ready for order management - no quote for {self.instrument.symbol}")
|
|
135
132
|
|
|
136
133
|
# - validate order parameters
|
|
137
134
|
self._validate_order(order_side, order_type, amount, price, time_in_force)
|
qubx/backtester/optimization.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Sequence, Tuple, Type
|
|
2
|
-
import numpy as np
|
|
3
1
|
import re
|
|
4
|
-
|
|
5
|
-
from types import FunctionType
|
|
6
2
|
from itertools import product
|
|
3
|
+
from types import FunctionType
|
|
4
|
+
from typing import Any, Dict, List, Sequence, Tuple, Type
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _wrap_single_list(param_grid: List | Dict) -> Dict[str, Any] | List:
|
|
@@ -167,7 +167,7 @@ def variate(clz: Type[Any] | List[Type[Any]], *args, conditions=None, **kwargs)
|
|
|
167
167
|
"""
|
|
168
168
|
|
|
169
169
|
def _cmprss(xs: str):
|
|
170
|
-
return "".join([x[0] for x in re.split("((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
|
|
170
|
+
return "".join([x[0] for x in re.split(r"((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
|
|
171
171
|
|
|
172
172
|
if isinstance(clz, type):
|
|
173
173
|
sfx = _cmprss(clz.__name__)
|
|
@@ -5,8 +5,7 @@ from typing import Any, Iterable, Iterator, TypeAlias
|
|
|
5
5
|
import pandas as pd
|
|
6
6
|
|
|
7
7
|
from qubx import logger
|
|
8
|
-
from qubx.core.basics import BatchEvent, DataType, Instrument,
|
|
9
|
-
from qubx.core.series import Bar, OrderBook, Quote, Trade
|
|
8
|
+
from qubx.core.basics import BatchEvent, DataType, Instrument, Timestamped, dt_64
|
|
10
9
|
from qubx.data.readers import (
|
|
11
10
|
AsDict,
|
|
12
11
|
AsQuotes,
|
|
@@ -18,8 +17,7 @@ from qubx.data.readers import (
|
|
|
18
17
|
RestoreTradesFromOHLC,
|
|
19
18
|
)
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
SlicerOutData: TypeAlias = tuple[str, int, InData] | tuple
|
|
20
|
+
SlicerOutData: TypeAlias = tuple[str, int, Timestamped] | tuple
|
|
23
21
|
|
|
24
22
|
|
|
25
23
|
class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
|
|
@@ -29,8 +27,8 @@ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
|
|
|
29
27
|
It supports adding / removing new data streams to the slicer on the fly (during the itration).
|
|
30
28
|
"""
|
|
31
29
|
|
|
32
|
-
_iterators: dict[str, Iterator[list[
|
|
33
|
-
_buffers: dict[str, list[
|
|
30
|
+
_iterators: dict[str, Iterator[list[Timestamped]]]
|
|
31
|
+
_buffers: dict[str, list[Timestamped]]
|
|
34
32
|
_keys: deque[str]
|
|
35
33
|
_iterating: bool
|
|
36
34
|
|
|
@@ -40,7 +38,7 @@ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
|
|
|
40
38
|
self._keys = deque()
|
|
41
39
|
self._iterating = False
|
|
42
40
|
|
|
43
|
-
def put(self, data: dict[str, Iterator[list[
|
|
41
|
+
def put(self, data: dict[str, Iterator[list[Timestamped]]]):
|
|
44
42
|
_rebuild = False
|
|
45
43
|
for k, vi in data.items():
|
|
46
44
|
if k not in self._keys:
|
|
@@ -85,10 +83,10 @@ class IteratedDataStreamsSlicer(Iterator[SlicerOutData]):
|
|
|
85
83
|
_init_seq = dict(sorted(_init_seq.items(), key=lambda item: item[1]))
|
|
86
84
|
self._keys = deque(_init_seq.keys())
|
|
87
85
|
|
|
88
|
-
def _load_next_chunk_to_buffer(self, index: str) -> list[
|
|
86
|
+
def _load_next_chunk_to_buffer(self, index: str) -> list[Timestamped]:
|
|
89
87
|
return list(reversed(next(self._iterators[index])))
|
|
90
88
|
|
|
91
|
-
def _pop_top(self, k: str) ->
|
|
89
|
+
def _pop_top(self, k: str) -> Timestamped:
|
|
92
90
|
v = (data := self._buffers[k]).pop()
|
|
93
91
|
if not data:
|
|
94
92
|
try:
|
|
@@ -226,8 +224,6 @@ class DataFetcher:
|
|
|
226
224
|
_requests = self._specs if not to_load else set(self._make_request_id(i) for i in to_load)
|
|
227
225
|
_r_iters = {}
|
|
228
226
|
|
|
229
|
-
# logger.debug(f"{self._fetcher_id} loading {_requests}")
|
|
230
|
-
|
|
231
227
|
for _r in _requests: # - TODO: replace this loop with multi-instrument request after DataReader refactoring
|
|
232
228
|
if _r in self._specs:
|
|
233
229
|
_start = pd.Timestamp(start)
|
|
@@ -247,7 +243,10 @@ class DataFetcher:
|
|
|
247
243
|
if self._timeframe:
|
|
248
244
|
_args["timeframe"] = self._timeframe
|
|
249
245
|
|
|
250
|
-
|
|
246
|
+
try:
|
|
247
|
+
_r_iters[self._fetcher_id + "." + _r] = self._reader.read(**_args) # type: ignore
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f">>> (DataFetcher::load) - failed to load <g>'{self._fetcher_id}'</g> data: {e}")
|
|
251
250
|
else:
|
|
252
251
|
raise IndexError(
|
|
253
252
|
f"Instrument {_r} is not subscribed for this data {self._requested_data_type} in {self._fetcher_id} !"
|
|
@@ -438,7 +437,7 @@ class IterableSimulationData(Iterator):
|
|
|
438
437
|
self._slicing_iterator = iter(self._slicer_ctrl)
|
|
439
438
|
return self
|
|
440
439
|
|
|
441
|
-
def __next__(self) -> tuple[Instrument, str,
|
|
440
|
+
def __next__(self) -> tuple[Instrument, str, Timestamped, bool]: # type: ignore
|
|
442
441
|
try:
|
|
443
442
|
while data := next(self._slicing_iterator): # type: ignore
|
|
444
443
|
k, t, v = data
|
qubx/backtester/simulator.py
CHANGED
|
@@ -5,15 +5,16 @@ import pandas as pd
|
|
|
5
5
|
from joblib import delayed
|
|
6
6
|
|
|
7
7
|
from qubx import QubxLogConfig, logger, lookup
|
|
8
|
-
from qubx.core.basics import DataType
|
|
8
|
+
from qubx.core.basics import DataType
|
|
9
9
|
from qubx.core.context import StrategyContext
|
|
10
10
|
from qubx.core.exceptions import SimulationError
|
|
11
11
|
from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
|
|
12
12
|
from qubx.core.interfaces import IStrategy
|
|
13
13
|
from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
|
|
14
|
+
from qubx.core.metrics import TradingSessionResult
|
|
14
15
|
from qubx.data.helpers import InMemoryCachedReader, TimeGuardedWrapper
|
|
15
16
|
from qubx.data.readers import DataReader
|
|
16
|
-
from qubx.utils.misc import ProgressParallel
|
|
17
|
+
from qubx.utils.misc import ProgressParallel, get_current_user
|
|
17
18
|
|
|
18
19
|
from .account import SimulatedAccountProcessor
|
|
19
20
|
from .broker import SimulatedBroker
|
|
@@ -226,7 +227,7 @@ def _run_setup(
|
|
|
226
227
|
accurate_stop_orders_execution=accurate_stop_orders_execution,
|
|
227
228
|
)
|
|
228
229
|
scheduler = SimulatedScheduler(channel, lambda: time_provider.time().item())
|
|
229
|
-
broker = SimulatedBroker(channel, account)
|
|
230
|
+
broker = SimulatedBroker(channel, account, setup.exchange)
|
|
230
231
|
data_provider = SimulatedDataProvider(
|
|
231
232
|
exchange_id=setup.exchange,
|
|
232
233
|
channel=channel,
|
|
@@ -346,4 +347,5 @@ def _run_setup(
|
|
|
346
347
|
strategy_class=_s_class,
|
|
347
348
|
parameters=_s_params,
|
|
348
349
|
is_simulation=True,
|
|
350
|
+
author=get_current_user(),
|
|
349
351
|
)
|
qubx/connectors/ccxt/broker.py
CHANGED
|
@@ -22,7 +22,7 @@ from .utils import ccxt_convert_order_info, instrument_to_ccxt_symbol
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
class CcxtBroker(IBroker):
|
|
25
|
-
|
|
25
|
+
_exchange: cxp.Exchange
|
|
26
26
|
|
|
27
27
|
_positions: dict[Instrument, Position]
|
|
28
28
|
_loop: AsyncThreadLoop
|
|
@@ -34,7 +34,7 @@ class CcxtBroker(IBroker):
|
|
|
34
34
|
time_provider: ITimeProvider,
|
|
35
35
|
account: IAccountProcessor,
|
|
36
36
|
):
|
|
37
|
-
self.
|
|
37
|
+
self._exchange = exchange
|
|
38
38
|
self.ccxt_exchange_id = str(exchange.name)
|
|
39
39
|
self.channel = channel
|
|
40
40
|
self.time_provider = time_provider
|
|
@@ -73,7 +73,7 @@ class CcxtBroker(IBroker):
|
|
|
73
73
|
r: dict[str, Any] | None = None
|
|
74
74
|
try:
|
|
75
75
|
r = self._loop.submit(
|
|
76
|
-
self.
|
|
76
|
+
self._exchange.create_order(
|
|
77
77
|
symbol=ccxt_symbol,
|
|
78
78
|
type=order_type, # type: ignore
|
|
79
79
|
side=order_side, # type: ignore
|
|
@@ -109,7 +109,7 @@ class CcxtBroker(IBroker):
|
|
|
109
109
|
try:
|
|
110
110
|
logger.info(f"Canceling order {order_id} ...")
|
|
111
111
|
r = self._loop.submit(
|
|
112
|
-
self.
|
|
112
|
+
self._exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(order.instrument))
|
|
113
113
|
).result()
|
|
114
114
|
except Exception as err:
|
|
115
115
|
logger.error(f"Canceling [{order}] exception : {err}")
|
|
@@ -122,3 +122,9 @@ class CcxtBroker(IBroker):
|
|
|
122
122
|
|
|
123
123
|
def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
|
|
124
124
|
raise NotImplementedError("Not implemented yet")
|
|
125
|
+
|
|
126
|
+
def exchange(self) -> str:
|
|
127
|
+
"""
|
|
128
|
+
Return the name of the exchange this broker is connected to.
|
|
129
|
+
"""
|
|
130
|
+
return self.ccxt_exchange_id.upper()
|
qubx/connectors/ccxt/data.py
CHANGED
|
@@ -423,6 +423,14 @@ class CcxtDataProvider(IDataProvider):
|
|
|
423
423
|
False, # not historical bar
|
|
424
424
|
)
|
|
425
425
|
)
|
|
426
|
+
if not (
|
|
427
|
+
self.has_subscription(instrument, DataType.ORDERBOOK)
|
|
428
|
+
or self.has_subscription(instrument, DataType.QUOTE)
|
|
429
|
+
):
|
|
430
|
+
_price = ohlcvs[-1][4]
|
|
431
|
+
_s2 = instrument.tick_size / 2.0
|
|
432
|
+
_bid, _ask = _price - _s2, _price + _s2
|
|
433
|
+
self._last_quotes[instrument] = Quote(oh[0] * 1_000_000, _bid, _ask, 0.0, 0.0)
|
|
426
434
|
|
|
427
435
|
# ohlc subscription reuses the same connection always, unsubscriptions don't work properly
|
|
428
436
|
# but it's likely not very needed
|
|
@@ -599,3 +607,6 @@ class CcxtDataProvider(IDataProvider):
|
|
|
599
607
|
name=name,
|
|
600
608
|
unsubscriber=un_watch_funding_rates,
|
|
601
609
|
)
|
|
610
|
+
|
|
611
|
+
def exchange(self) -> str:
|
|
612
|
+
return self._exchange_id.upper()
|
qubx/core/basics.py
CHANGED
|
@@ -3,26 +3,59 @@ from datetime import datetime
|
|
|
3
3
|
from enum import StrEnum
|
|
4
4
|
from queue import Empty, Queue
|
|
5
5
|
from threading import Event, Lock
|
|
6
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, Literal, Optional, TypeAlias, Union
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
10
10
|
|
|
11
11
|
from qubx.core.exceptions import QueueTimeout
|
|
12
|
-
from qubx.core.series import Quote, Trade, time_as_nsec
|
|
12
|
+
from qubx.core.series import Bar, OrderBook, Quote, Trade, time_as_nsec
|
|
13
13
|
from qubx.core.utils import prec_ceil, prec_floor, time_delta_to_str
|
|
14
|
-
from qubx.utils.misc import Stopwatch
|
|
14
|
+
from qubx.utils.misc import Stopwatch, version
|
|
15
15
|
from qubx.utils.ntp import start_ntp_thread, time_now
|
|
16
16
|
|
|
17
17
|
dt_64 = np.datetime64
|
|
18
18
|
td_64 = np.timedelta64
|
|
19
|
-
ns_to_dt_64 = lambda ns: np.datetime64(ns, "ns")
|
|
20
19
|
|
|
21
20
|
OPTION_FILL_AT_SIGNAL_PRICE = "fill_at_signal_price"
|
|
22
21
|
|
|
23
22
|
SW = Stopwatch()
|
|
24
23
|
|
|
25
24
|
|
|
25
|
+
@dataclass
|
|
26
|
+
class Liquidation:
|
|
27
|
+
time: dt_64
|
|
28
|
+
quantity: float
|
|
29
|
+
price: float
|
|
30
|
+
side: int
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class FundingRate:
|
|
35
|
+
time: dt_64
|
|
36
|
+
rate: float
|
|
37
|
+
interval: str
|
|
38
|
+
next_funding_time: dt_64
|
|
39
|
+
mark_price: float | None = None
|
|
40
|
+
index_price: float | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class TimestampedDict:
|
|
45
|
+
"""
|
|
46
|
+
Generic class for representing arbitrary data (as dict) with timestamp
|
|
47
|
+
|
|
48
|
+
TODO: probably we need to have generic interface for classes like Quote, Bar, .... etc
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
time: dt_64
|
|
52
|
+
data: dict[str, Any]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Alias for timestamped data types used in Qubx
|
|
56
|
+
Timestamped: TypeAlias = Quote | Trade | Bar | OrderBook | TimestampedDict | FundingRate | Liquidation
|
|
57
|
+
|
|
58
|
+
|
|
26
59
|
@dataclass
|
|
27
60
|
class Signal:
|
|
28
61
|
"""
|
|
@@ -240,7 +273,7 @@ class Instrument:
|
|
|
240
273
|
@dataclass
|
|
241
274
|
class BatchEvent:
|
|
242
275
|
time: dt_64 | pd.Timestamp
|
|
243
|
-
data: list[
|
|
276
|
+
data: list[Timestamped]
|
|
244
277
|
|
|
245
278
|
|
|
246
279
|
class TransactionCostsCalculator:
|
|
@@ -798,116 +831,6 @@ class DataType(StrEnum):
|
|
|
798
831
|
return "(" in value
|
|
799
832
|
|
|
800
833
|
|
|
801
|
-
class TradingSessionResult:
|
|
802
|
-
# fmt: off
|
|
803
|
-
id: int
|
|
804
|
-
name: str
|
|
805
|
-
start: str | pd.Timestamp
|
|
806
|
-
stop: str | pd.Timestamp
|
|
807
|
-
exchange: str # exchange name (TODO: need to think how to do with it for multiple exchanges)
|
|
808
|
-
instruments: list[Instrument] # instruments used at the start of the session (TODO: need to collect all traded instruments)
|
|
809
|
-
capital: float
|
|
810
|
-
leverage: float
|
|
811
|
-
base_currency: str
|
|
812
|
-
commissions: str # used commissions ("vip0_usdt" etc)
|
|
813
|
-
portfolio_log: pd.DataFrame # portfolio log records
|
|
814
|
-
executions_log: pd.DataFrame # executed trades
|
|
815
|
-
signals_log: pd.DataFrame # signals generated by the strategy
|
|
816
|
-
strategy_class: str # strategy full qualified class name
|
|
817
|
-
parameters: dict[str, Any] # strategy parameters if provided
|
|
818
|
-
is_simulation: bool
|
|
819
|
-
# fmt: on
|
|
820
|
-
|
|
821
|
-
def __init__(
|
|
822
|
-
self,
|
|
823
|
-
id: int,
|
|
824
|
-
name: str,
|
|
825
|
-
start: str | pd.Timestamp,
|
|
826
|
-
stop: str | pd.Timestamp,
|
|
827
|
-
exchange: str,
|
|
828
|
-
instruments: list[Instrument],
|
|
829
|
-
capital: float,
|
|
830
|
-
leverage: float,
|
|
831
|
-
base_currency: str,
|
|
832
|
-
commissions: str,
|
|
833
|
-
portfolio_log: pd.DataFrame,
|
|
834
|
-
executions_log: pd.DataFrame,
|
|
835
|
-
signals_log: pd.DataFrame,
|
|
836
|
-
strategy_class: str,
|
|
837
|
-
parameters: dict[str, Any] | None = None,
|
|
838
|
-
is_simulation=True,
|
|
839
|
-
):
|
|
840
|
-
self.id = id
|
|
841
|
-
self.name = name
|
|
842
|
-
self.start = start
|
|
843
|
-
self.stop = stop
|
|
844
|
-
self.exchange = exchange
|
|
845
|
-
self.instruments = instruments
|
|
846
|
-
self.capital = capital
|
|
847
|
-
self.leverage = leverage
|
|
848
|
-
self.base_currency = base_currency
|
|
849
|
-
self.commissions = commissions
|
|
850
|
-
self.portfolio_log = portfolio_log
|
|
851
|
-
self.executions_log = executions_log
|
|
852
|
-
self.signals_log = signals_log
|
|
853
|
-
self.strategy_class = strategy_class
|
|
854
|
-
self.parameters = parameters if parameters else {}
|
|
855
|
-
self.is_simulation = is_simulation
|
|
856
|
-
|
|
857
|
-
@property
|
|
858
|
-
def symbols(self) -> list[str]:
|
|
859
|
-
"""
|
|
860
|
-
Extracts all traded symbols from the portfolio log
|
|
861
|
-
"""
|
|
862
|
-
if not self.portfolio_log.empty:
|
|
863
|
-
return list(set(self.portfolio_log.columns.str.split("_").str.get(0).values))
|
|
864
|
-
return []
|
|
865
|
-
|
|
866
|
-
def config(self, short=True) -> str:
|
|
867
|
-
"""
|
|
868
|
-
Return configuration as string: "test.strategies.Strategy1(parameter1=12345)"
|
|
869
|
-
TODO: probably we need to return recreated new object
|
|
870
|
-
"""
|
|
871
|
-
_cfg = ""
|
|
872
|
-
if self.strategy_class:
|
|
873
|
-
_params = ", ".join([f"{k}={repr(v)}" for k, v in self.parameters.items()])
|
|
874
|
-
_class = self.strategy_class.split(".")[-1] if short else self.strategy_class
|
|
875
|
-
_cfg = f"{_class}({_params})"
|
|
876
|
-
# _cfg = f"{{ {repr(self.name)}: {_class}({_params}) }}"
|
|
877
|
-
# if instantiated: return eval(_cfg)
|
|
878
|
-
return _cfg
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
@dataclass
|
|
882
|
-
class Liquidation:
|
|
883
|
-
time: dt_64
|
|
884
|
-
quantity: float
|
|
885
|
-
price: float
|
|
886
|
-
side: int
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
@dataclass
|
|
890
|
-
class FundingRate:
|
|
891
|
-
time: dt_64
|
|
892
|
-
rate: float
|
|
893
|
-
interval: str
|
|
894
|
-
next_funding_time: dt_64
|
|
895
|
-
mark_price: float | None = None
|
|
896
|
-
index_price: float | None = None
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
@dataclass
|
|
900
|
-
class TimestampedDict:
|
|
901
|
-
"""
|
|
902
|
-
Generic class for representing arbitrary data (as dict) with timestamp
|
|
903
|
-
|
|
904
|
-
TODO: probably we need to have gebneric interface for classes like Quote, Bar, .... etc
|
|
905
|
-
"""
|
|
906
|
-
|
|
907
|
-
time: dt_64
|
|
908
|
-
data: dict[str, Any]
|
|
909
|
-
|
|
910
|
-
|
|
911
834
|
class LiveTimeProvider(ITimeProvider):
|
|
912
835
|
def __init__(self):
|
|
913
836
|
self._start_ntp_thread()
|
qubx/core/context.py
CHANGED
|
@@ -102,7 +102,10 @@ class StrategyContext(IStrategyContext):
|
|
|
102
102
|
|
|
103
103
|
__position_gathering = position_gathering if position_gathering is not None else SimplePositionGatherer()
|
|
104
104
|
|
|
105
|
-
self._subscription_manager = SubscriptionManager(
|
|
105
|
+
self._subscription_manager = SubscriptionManager(
|
|
106
|
+
data_provider=self._data_provider,
|
|
107
|
+
default_base_subscription=DataType.ORDERBOOK if not self._data_provider.is_simulation else DataType.NONE,
|
|
108
|
+
)
|
|
106
109
|
self.account.set_subscription_manager(self._subscription_manager)
|
|
107
110
|
|
|
108
111
|
self._market_data_provider = MarketManager(
|
|
@@ -331,6 +334,10 @@ class StrategyContext(IStrategyContext):
|
|
|
331
334
|
def instruments(self):
|
|
332
335
|
return self._universe_manager.instruments
|
|
333
336
|
|
|
337
|
+
@property
|
|
338
|
+
def exchanges(self) -> list[str]:
|
|
339
|
+
return self._trading_manager.exchanges()
|
|
340
|
+
|
|
334
341
|
# ISubscriptionManager delegation
|
|
335
342
|
def subscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None):
|
|
336
343
|
return self._subscription_manager.subscribe(subscription_type, instruments)
|