Qubx 0.2.77__cp311-cp311-manylinux_2_35_x86_64.whl → 0.2.79__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
@@ -6,6 +6,8 @@ from loguru import logger
6
6
  import os, sys, stackprinter
7
7
  from qubx.core.lookups import FeesLookup, GlobalLookup, InstrumentsLookup
8
8
 
9
+ # - TODO: import some main methods from packages
10
+
9
11
 
10
12
  def formatter(record):
11
13
  end = record["extra"].get("end", "\n")
@@ -63,7 +65,7 @@ lookup = GlobalLookup(InstrumentsLookup(), FeesLookup())
63
65
  # registering magic for jupyter notebook
64
66
  if runtime_env() in ["notebook", "shell"]:
65
67
  from IPython.core.magic import Magics, magics_class, line_magic, line_cell_magic
66
- from IPython import get_ipython
68
+ from IPython.core.getipython import get_ipython
67
69
 
68
70
  @magics_class
69
71
  class QubxMagics(Magics):
@@ -195,4 +197,4 @@ if runtime_env() in ["notebook", "shell"]:
195
197
  p.terminate()
196
198
 
197
199
  # - registering magic here
198
- get_ipython().register_magics(QubxMagics)
200
+ get_ipython().register_magics(QubxMagics) # type: ignore
qubx/_nb_magic.py CHANGED
@@ -65,6 +65,7 @@ if runtime_env() in ["notebook", "shell"]:
65
65
  AsTimestampedRecords,
66
66
  RestoreTicksFromOHLC,
67
67
  )
68
+ from qubx.data.helpers import loader
68
69
 
69
70
  # - - - - Simulator stuff - - - -
70
71
  from qubx.backtester.simulator import simulate
@@ -37,6 +37,7 @@ from qubx.core.strategy import (
37
37
  from qubx.core.context import StrategyContextImpl
38
38
  from qubx.backtester.ome import OrdersManagementEngine, OmeReport
39
39
 
40
+ from qubx.data.helpers import InMemoryCachedReader, TimeGuardedWrapper
40
41
  from qubx.data.readers import (
41
42
  AsTrades,
42
43
  DataReader,
@@ -428,7 +429,9 @@ class SimulatedExchange(IBrokerServiceProvider):
428
429
  units = kwargs.get("timestamp_units", "ns")
429
430
 
430
431
  for instr in instruments:
431
- logger.debug(f"SimulatedExchangeService :: subscribe :: {instr.symbol} :: {subscription_type}")
432
+ logger.debug(
433
+ f"SimulatedExchangeService :: subscribe :: {instr.symbol}({subscription_type}, {nback}, {timeframe})"
434
+ )
432
435
  self._symbol_to_instrument[instr.symbol] = instr
433
436
 
434
437
  _params: Dict[str, Any] = dict(
@@ -731,6 +734,7 @@ def simulate(
731
734
  silent: bool = False,
732
735
  enable_event_batching: bool = True,
733
736
  accurate_stop_orders_execution: bool = False,
737
+ aux_data: DataReader | None = None,
734
738
  debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
735
739
  ) -> list[TradingSessionResult]:
736
740
  """
@@ -773,6 +777,8 @@ def simulate(
773
777
  If True, enables event batching for optimization.
774
778
  accurate_stop_orders_execution (bool):
775
779
  If True, enables more accurate stop order execution simulation.
780
+ aux_data (DataReader | None):
781
+ Auxiliary data provider (default is None).
776
782
  debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None):
777
783
  Logging level for debugging.
778
784
 
@@ -842,6 +848,7 @@ def simulate(
842
848
  silent=silent,
843
849
  enable_event_batching=enable_event_batching,
844
850
  accurate_stop_orders_execution=accurate_stop_orders_execution,
851
+ aux_data=aux_data,
845
852
  )
846
853
 
847
854
 
@@ -906,6 +913,7 @@ def _run_setups(
906
913
  silent: bool = False,
907
914
  enable_event_batching: bool = True,
908
915
  accurate_stop_orders_execution: bool = False,
916
+ aux_data: DataReader | None = None,
909
917
  ) -> List[TradingSessionResult]:
910
918
  # loggers don't work well with joblib and multiprocessing in general because they contain
911
919
  # open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
@@ -928,6 +936,7 @@ def _run_setups(
928
936
  silent=silent,
929
937
  enable_event_batching=enable_event_batching,
930
938
  accurate_stop_orders_execution=accurate_stop_orders_execution,
939
+ aux_data_provider=aux_data,
931
940
  )
932
941
  for id, s in enumerate(setups)
933
942
  )
@@ -946,6 +955,7 @@ def _run_setup(
946
955
  silent: bool = False,
947
956
  enable_event_batching: bool = True,
948
957
  accurate_stop_orders_execution: bool = False,
958
+ aux_data_provider: InMemoryCachedReader | None = None,
949
959
  ) -> TradingSessionResult:
950
960
  _trigger = trigger
951
961
  _stop = stop
@@ -996,6 +1006,13 @@ def _run_setup(
996
1006
  case _:
997
1007
  raise ValueError(f"Unsupported setup type: {setup.setup_type} !")
998
1008
 
1009
+ # - check aux data provider
1010
+ _aux_data = None
1011
+ if aux_data_provider is not None:
1012
+ if not isinstance(aux_data_provider, InMemoryCachedReader):
1013
+ logger.error("Aux data provider should be an instance of InMemoryCachedReader! Skipping it.")
1014
+ _aux_data = TimeGuardedWrapper(aux_data_provider, broker)
1015
+
999
1016
  ctx = StrategyContextImpl(
1000
1017
  strategy=strat, # type: ignore
1001
1018
  config=None, # TODO: need to think how we could pass altered parameters here (from variating etc)
@@ -1007,6 +1024,7 @@ def _run_setup(
1007
1024
  trigger_spec=_trigger,
1008
1025
  fit_spec=fit,
1009
1026
  logs_writer=logs_writer,
1027
+ aux_data_provider=_aux_data,
1010
1028
  )
1011
1029
  ctx.start()
1012
1030
 
qubx/core/context.py CHANGED
@@ -36,6 +36,7 @@ from qubx.core.strategy import (
36
36
  SubscriptionType,
37
37
  )
38
38
  from qubx.core.series import Trade, Quote, Bar, OHLCV
39
+ from qubx.data.readers import DataReader
39
40
  from qubx.gathering.simplest import SimplePositionGatherer
40
41
  from qubx.trackers.sizers import FixedSizer
41
42
  from qubx.utils.misc import Stopwatch
@@ -74,6 +75,9 @@ class StrategyContextImpl(StrategyContext):
74
75
  _cache: CachedMarketDataHolder # market data cache
75
76
  _scheduler: BasicScheduler
76
77
 
78
+ # - aux data provider
79
+ _aux_data_provider: DataReader | None # auxiliary data provider
80
+
77
81
  # - configuration
78
82
  _market_data_subcription_type: str = "unknown"
79
83
  _market_data_subcription_params: dict = dict()
@@ -126,6 +130,9 @@ class StrategyContextImpl(StrategyContext):
126
130
  # - - - - - - - - - - - - - - - - - - - - -
127
131
  # - signals executor configuration - - - -
128
132
  position_gathering: IPositionGathering | None = None,
133
+ # - - - - - - - - - - - - - - - - - - - - -
134
+ # - aux data provider - - - - - - - - - - -
135
+ aux_data_provider: DataReader | None = None,
129
136
  ) -> None:
130
137
  # - initialization
131
138
  self.broker_provider = broker_connector
@@ -146,6 +153,7 @@ class StrategyContextImpl(StrategyContext):
146
153
  self.__init_fit_was_called = False
147
154
  self.__pool = None
148
155
  self.__fails_counter = 0
156
+ self._aux_data_provider = aux_data_provider
149
157
 
150
158
  # - for fast access to instrument by it's symbol
151
159
  self._symb_to_instr = {i.symbol: i for i in instruments}
@@ -751,36 +759,12 @@ class StrategyContextImpl(StrategyContext):
751
759
  if order_id:
752
760
  self.trading_service.cancel_order(order_id)
753
761
 
754
- def quote(self, symbol: str) -> Quote | None:
755
- return self.broker_provider.get_quote(symbol)
756
-
757
762
  def get_capital(self) -> float:
758
763
  return self.trading_service.get_capital()
759
764
 
760
765
  def get_reserved(self, instrument: Instrument) -> float:
761
766
  return self.trading_service.get_account().get_reserved(instrument)
762
767
 
763
- @_SW.watch("StrategyContext")
764
- def get_historical_ohlcs(self, instrument: Instrument | str, timeframe: str, length: int) -> OHLCV | None:
765
- """
766
- Helper for historical ohlc data
767
- """
768
- instr = self._symb_to_instr.get(instrument) if isinstance(instrument, str) else instrument
769
-
770
- if instr is None:
771
- logger.warning(f"Can't find instrument for {instrument} symbol !")
772
- return None
773
-
774
- # - first check if we can use cached series
775
- rc = self.ohlc(instr, timeframe)
776
- if len(rc) >= length:
777
- return rc
778
-
779
- # - send request for historical data
780
- bars = self.broker_provider.get_historical_ohlcs(instr.symbol, timeframe, length)
781
- r = self._cache.update_by_bars(instr.symbol, timeframe, bars)
782
- return r
783
-
784
768
  @_SW.watch("StrategyContext")
785
769
  def set_universe(self, instruments: list[Instrument]) -> None:
786
770
  for instr in instruments:
@@ -901,3 +885,40 @@ class StrategyContextImpl(StrategyContext):
901
885
  if self.__pool is None:
902
886
  self.__pool = ThreadPool(2)
903
887
  self.__pool.apply_async(func, args)
888
+
889
+ # - IMarketDataProvider methods implementation -
890
+
891
+ @_SW.watch("StrategyContext")
892
+ def get_historical_ohlcs(self, instrument: Instrument | str, timeframe: str, length: int) -> OHLCV | None:
893
+ """
894
+ Helper for historical ohlc data
895
+ """
896
+ instr = self._symb_to_instr.get(instrument) if isinstance(instrument, str) else instrument
897
+
898
+ if instr is None:
899
+ logger.warning(f"Can't find instrument for {instrument} symbol !")
900
+ return None
901
+
902
+ # - first check if we can use cached series
903
+ rc = self.ohlc(instr, timeframe)
904
+ if len(rc) >= length:
905
+ return rc
906
+
907
+ # - send request for historical data
908
+ bars = self.broker_provider.get_historical_ohlcs(instr.symbol, timeframe, length)
909
+ r = self._cache.update_by_bars(instr.symbol, timeframe, bars)
910
+ return r
911
+
912
+ def get_aux_data(self, data_id: str, **parameters) -> pd.DataFrame | None:
913
+ if self._aux_data_provider:
914
+ return self._aux_data_provider.get_aux_data(data_id, **parameters)
915
+ return None
916
+
917
+ def get_instruments(self) -> List[Instrument]:
918
+ return self.instruments
919
+
920
+ def quote(self, symbol: str) -> Quote | None:
921
+ return self.broker_provider.get_quote(symbol)
922
+
923
+ def get_instrument(self, symbol: str) -> Instrument | None:
924
+ return self._symb_to_instr.get(symbol)
qubx/core/metrics.py CHANGED
@@ -340,7 +340,7 @@ def aggregate_returns(returns: pd.Series, convert_to: str) -> pd.DataFrame | pd.
340
340
  str_check = convert_to.lower()
341
341
  resample_mod = None
342
342
  if str_check in ["a", "annual", "y", "yearly"]:
343
- resample_mod = "A"
343
+ resample_mod = "YE"
344
344
  elif str_check in ["m", "monthly", "mon"]:
345
345
  resample_mod = "ME"
346
346
  elif str_check in ["w", "weekly"]:
qubx/core/series.pyi CHANGED
@@ -97,5 +97,6 @@ class IndicatorOHLC(Indicator):
97
97
  def time_as_nsec(time: Any) -> np.datetime64: ...
98
98
 
99
99
  class RollingSum:
100
+ is_init_stage: bool
100
101
  def __init__(self, period: int) -> None: ...
101
102
  def update(self, value: float, new_item_started: bool) -> float: ...
qubx/core/strategy.py CHANGED
@@ -3,11 +3,6 @@
3
3
  """
4
4
 
5
5
  from typing import Any, Callable, Dict, List, Optional, Tuple, Union
6
- from types import FunctionType
7
- from collections import defaultdict
8
- from dataclasses import dataclass
9
- from threading import Thread
10
- from multiprocessing.pool import ThreadPool
11
6
  import traceback
12
7
 
13
8
  import pandas as pd
@@ -30,28 +25,58 @@ from qubx.core.basics import (
30
25
  )
31
26
  from qubx.core.series import Trade, Quote, Bar, OHLCV
32
27
  from qubx.utils.misc import Stopwatch
33
- from qubx.utils.time import convert_seconds_to_str
34
28
 
35
29
 
36
30
  _SW = Stopwatch()
37
31
 
38
32
 
39
33
  class ITradingServiceProvider(ITimeProvider, IComminucationManager):
34
+ """
35
+ Trading service provider interface that manages account operations, order placement, and position tracking.
36
+ """
37
+
40
38
  acc: AccountProcessor
41
39
 
42
40
  def set_account(self, account: AccountProcessor):
41
+ """
42
+ Set the account processor for the trading service provider.
43
+
44
+ :param account: The AccountProcessor object to be set.
45
+ """
43
46
  self.acc = account
44
47
 
45
48
  def get_account(self) -> AccountProcessor:
49
+ """
50
+ Retrieve the current account processor.
51
+
52
+ :return: The current AccountProcessor object.
53
+ """
46
54
  return self.acc
47
55
 
48
56
  def get_name(self) -> str:
57
+ """
58
+ Get the name of the trading service provider.
59
+
60
+ :return: The name of the trading service provider as a string.
61
+ :raises NotImplementedError: If the method is not implemented by the subclass.
62
+ """
49
63
  raise NotImplementedError("get_name is not implemented")
50
64
 
51
65
  def get_account_id(self) -> str:
66
+ """
67
+ Get the account ID associated with the trading service provider.
68
+
69
+ :return: The account ID as a string.
70
+ :raises NotImplementedError: If the method is not implemented by the subclass.
71
+ """
52
72
  raise NotImplementedError("get_account_id is not implemented")
53
73
 
54
74
  def get_capital(self) -> float:
75
+ """
76
+ Get the available capital in the account.
77
+
78
+ :return: The free capital as a float.
79
+ """
55
80
  return self.acc.get_free_capital()
56
81
 
57
82
  def send_order(
@@ -65,25 +90,81 @@ class ITradingServiceProvider(ITimeProvider, IComminucationManager):
65
90
  time_in_force: str = "gtc",
66
91
  **optional,
67
92
  ) -> Order:
93
+ """
94
+ Send an order to the trading service.
95
+
96
+ :param instrument: The instrument to trade.
97
+ :param order_side: The side of the order (e.g., "buy" or "sell").
98
+ :param order_type: The type of the order (e.g., "market" or "limit").
99
+ :param amount: The amount of the instrument to trade.
100
+ :param price: The price for limit orders (optional).
101
+ :param client_id: A client-specified ID for the order (optional).
102
+ :param time_in_force: The time in force for the order (default is "gtc" - good till cancelled).
103
+ :param optional: Additional optional parameters for the order.
104
+ :return: An Order object representing the sent order.
105
+ :raises NotImplementedError: If the method is not implemented by the subclass.
106
+ """
68
107
  raise NotImplementedError("send_order is not implemented")
69
108
 
70
109
  def cancel_order(self, order_id: str) -> Order | None:
110
+ """
111
+ Cancel an existing order.
112
+
113
+ :param order_id: The ID of the order to cancel.
114
+ :return: The cancelled Order object if successful, None otherwise.
115
+ :raises NotImplementedError: If the method is not implemented by the subclass.
116
+ """
71
117
  raise NotImplementedError("cancel_order is not implemented")
72
118
 
73
119
  def get_orders(self, symbol: str | None = None) -> List[Order]:
120
+ """
121
+ Get a list of current orders, optionally filtered by symbol.
122
+
123
+ :param symbol: The symbol to filter orders by (optional).
124
+ :return: A list of Order objects.
125
+ :raises NotImplementedError: If the method is not implemented by the subclass.
126
+ """
74
127
  raise NotImplementedError("get_orders is not implemented")
75
128
 
76
129
  def get_position(self, instrument: Instrument | str) -> Position:
130
+ """
131
+ Get the current position for a given instrument.
132
+
133
+ :param instrument: The instrument or symbol to get the position for.
134
+ :return: A Position object representing the current position.
135
+ :raises NotImplementedError: If the method is not implemented by the subclass.
136
+ """
77
137
  raise NotImplementedError("get_position is not implemented")
78
138
 
79
139
  def get_base_currency(self) -> str:
140
+ """
141
+ Get the base currency for the account.
142
+
143
+ :return: The base currency as a string.
144
+ :raises NotImplementedError: If the method is not implemented by the subclass.
145
+ """
80
146
  raise NotImplementedError("get_basic_currency is not implemented")
81
147
 
82
148
  def process_execution_report(self, symbol: str, report: Dict[str, Any]) -> Tuple[Order, List[Deal]]:
149
+ """
150
+ Process an execution report for a given symbol.
151
+
152
+ :param symbol: The symbol the execution report is for.
153
+ :param report: A dictionary containing the execution report details.
154
+ :return: A tuple containing the updated Order and a list of Deal objects.
155
+ :raises NotImplementedError: If the method is not implemented by the subclass.
156
+ """
83
157
  raise NotImplementedError("process_execution_report is not implemented")
84
158
 
85
159
  @staticmethod
86
160
  def _extract_price(update: float | Quote | Trade | Bar) -> float:
161
+ """
162
+ Extract the price from various types of market data updates.
163
+
164
+ :param update: The market data update, which can be a float, Quote, Trade, or Bar.
165
+ :return: The extracted price as a float.
166
+ :raises ValueError: If the update type is unknown.
167
+ """
87
168
  if isinstance(update, float):
88
169
  return update
89
170
  elif isinstance(update, Quote):
@@ -96,6 +177,13 @@ class ITradingServiceProvider(ITimeProvider, IComminucationManager):
96
177
  raise ValueError(f"Unknown update type: {type(update)}")
97
178
 
98
179
  def update_position_price(self, symbol: str, timestamp: dt_64, update: float | Quote | Trade | Bar):
180
+ """
181
+ Update the price of a position for a given symbol.
182
+
183
+ :param symbol: The symbol of the position to update.
184
+ :param timestamp: The timestamp of the update.
185
+ :param update: The price update, which can be a float, Quote, Trade, or Bar.
186
+ """
99
187
  self.acc.update_position_price(timestamp, symbol, ITradingServiceProvider._extract_price(update))
100
188
 
101
189
 
@@ -146,7 +234,25 @@ class SubscriptionType:
146
234
  OHLC = "ohlc"
147
235
 
148
236
 
149
- class StrategyContext(ITimeProvider):
237
+ class IMarketDataProvider(ITimeProvider):
238
+ """
239
+ Interface for market data providing class
240
+ """
241
+
242
+ def ohlc(self, instrument: Instrument | str, timeframe: str) -> OHLCV: ...
243
+
244
+ def quote(self, symbol: str) -> Quote | None: ...
245
+
246
+ def get_historical_ohlcs(self, instrument: Instrument | str, timeframe: str, length: int) -> OHLCV | None: ...
247
+
248
+ def get_aux_data(self, data_id: str, **parametes) -> pd.DataFrame | None: ...
249
+
250
+ def get_instruments(self) -> List[Instrument]: ...
251
+
252
+ def get_instrument(self, symbol: str) -> Instrument | None: ...
253
+
254
+
255
+ class StrategyContext(IMarketDataProvider):
150
256
  """
151
257
  Strategy context interface
152
258
  """
@@ -154,17 +260,14 @@ class StrategyContext(ITimeProvider):
154
260
  instruments: List[Instrument] # list of instruments this strategy trades
155
261
  positions: Dict[str, Position] # positions of the strategy (instrument -> position)
156
262
  acc: AccountProcessor
263
+ broker_provider: IBrokerServiceProvider # market data provider
157
264
 
158
265
  def process_data(self, symbol: str, d_type: str, data: Any) -> bool: ...
159
266
 
160
- def ohlc(self, instrument: str | Instrument, timeframe: str) -> OHLCV: ...
161
-
162
267
  def start(self, blocking: bool = False): ...
163
268
 
164
269
  def stop(self): ...
165
270
 
166
- def time(self) -> dt_64: ...
167
-
168
271
  def trade(
169
272
  self,
170
273
  instr_or_symbol: Instrument | str,
@@ -178,14 +281,10 @@ class StrategyContext(ITimeProvider):
178
281
 
179
282
  def cancel_order(self, order_id: str): ...
180
283
 
181
- def quote(self, symbol: str) -> Quote | None: ...
182
-
183
284
  def get_capital(self) -> float: ...
184
285
 
185
286
  def get_reserved(self, instrument: Instrument) -> float: ...
186
287
 
187
- def get_historical_ohlcs(self, instrument: Instrument | str, timeframe: str, length: int) -> OHLCV | None: ...
188
-
189
288
  def set_universe(self, instruments: list[Instrument]): ...
190
289
 
191
290
  def subscribe(self, subscription_type: str, instr_or_symbol: Instrument | str, **kwargs) -> bool: ...
qubx/data/__init__.py CHANGED
@@ -9,3 +9,5 @@ from .readers import (
9
9
  AsTimestampedRecords,
10
10
  RestoreTicksFromOHLC,
11
11
  )
12
+
13
+ from .helpers import loader