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 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
- import os, sys, stackprinter
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 time, re
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
- from datetime import time, timedelta
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()
@@ -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 | Trade | Bar
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
@@ -267,3 +267,6 @@ class SimulatedDataProvider(IDataProvider):
267
267
  cc.send((instrument, data_type, data, is_hist))
268
268
 
269
269
  return cc.control.is_set()
270
+
271
+ def exchange(self) -> str:
272
+ return self._exchange_id.upper()
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)
@@ -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, TimestampedDict, dt_64
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
- InData: TypeAlias = Quote | Trade | Bar | OrderBook | TimestampedDict
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[InData]]]
33
- _buffers: dict[str, list[InData]]
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[InData]]]):
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[InData]:
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) -> InData:
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
- _r_iters[self._fetcher_id + "." + _r] = self._reader.read(**_args) # type: ignore
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, InData, bool]: # type: ignore
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
@@ -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, TradingSessionResult
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
  )
@@ -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
- exchange: cxp.Exchange
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.exchange = exchange
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.exchange.create_order(
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.exchange.cancel_order(order_id, symbol=instrument_to_ccxt_symbol(order.instrument))
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()
@@ -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, Callable, Dict, List, Literal, Optional, Tuple, Union
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[Any]
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(data_provider=self._data_provider)
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)