Qubx 0.2.71__tar.gz → 0.2.73__tar.gz

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.

Files changed (56) hide show
  1. {qubx-0.2.71 → qubx-0.2.73}/PKG-INFO +1 -1
  2. {qubx-0.2.71 → qubx-0.2.73}/pyproject.toml +1 -1
  3. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/backtester/ome.py +12 -3
  4. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/backtester/simulator.py +99 -6
  5. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/series.pyi +6 -1
  6. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/gathering/simplest.py +2 -2
  7. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/trackers/riskctrl.py +7 -3
  8. {qubx-0.2.71 → qubx-0.2.73}/README.md +0 -0
  9. {qubx-0.2.71 → qubx-0.2.73}/build.py +0 -0
  10. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/__init__.py +0 -0
  11. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/_nb_magic.py +0 -0
  12. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/backtester/__init__.py +0 -0
  13. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/backtester/optimization.py +0 -0
  14. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/backtester/queue.py +0 -0
  15. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/__init__.py +0 -0
  16. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/account.py +0 -0
  17. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/basics.py +0 -0
  18. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/context.py +0 -0
  19. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/exceptions.py +0 -0
  20. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/helpers.py +0 -0
  21. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/loggers.py +0 -0
  22. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/lookups.py +0 -0
  23. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/metrics.py +0 -0
  24. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/series.pxd +0 -0
  25. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/series.pyx +0 -0
  26. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/strategy.py +0 -0
  27. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/utils.pyi +0 -0
  28. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/core/utils.pyx +0 -0
  29. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/data/helpers.py +0 -0
  30. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/data/readers.py +0 -0
  31. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/impl/ccxt_connector.py +0 -0
  32. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/impl/ccxt_customizations.py +0 -0
  33. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/impl/ccxt_trading.py +0 -0
  34. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/impl/ccxt_utils.py +0 -0
  35. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/math/__init__.py +0 -0
  36. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/math/stats.py +0 -0
  37. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/pandaz/__init__.py +0 -0
  38. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/pandaz/ta.py +0 -0
  39. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/pandaz/utils.py +0 -0
  40. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/ta/__init__.py +0 -0
  41. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/ta/indicators.pxd +0 -0
  42. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/ta/indicators.pyi +0 -0
  43. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/ta/indicators.pyx +0 -0
  44. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/trackers/__init__.py +0 -0
  45. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/trackers/composite.py +0 -0
  46. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/trackers/rebalancers.py +0 -0
  47. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/trackers/sizers.py +0 -0
  48. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/__init__.py +0 -0
  49. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/_pyxreloader.py +0 -0
  50. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/charting/lookinglass.py +0 -0
  51. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  52. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/marketdata/binance.py +0 -0
  53. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/misc.py +0 -0
  54. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/ntp.py +0 -0
  55. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/runner.py +0 -0
  56. {qubx-0.2.71 → qubx-0.2.73}/src/qubx/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.2.71
3
+ Version: 0.2.73
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.2.71"
3
+ version = "0.2.73"
4
4
  description = "Qubx - quantitative trading framework"
5
5
  authors = ["Dmitry Marienko <dmitry@gmail.com>", "Yuriy Arabskyy <yuriy.arabskyy@gmail.com>"]
6
6
  readme = "README.md"
@@ -43,9 +43,15 @@ class OrdersManagementEngine:
43
43
  bbo: Quote | None # current best bid/ask order book (simplest impl)
44
44
  __order_id: int
45
45
  __trade_id: int
46
+ _fill_stops_at_price: bool
46
47
 
47
48
  def __init__(
48
- self, instrument: Instrument, time_provider: ITimeProvider, tcc: TransactionCostsCalculator, debug: bool = True
49
+ self,
50
+ instrument: Instrument,
51
+ time_provider: ITimeProvider,
52
+ tcc: TransactionCostsCalculator,
53
+ fill_stop_order_at_price: bool = False, # emulate stop orders execution at order's exact limit price
54
+ debug: bool = True,
49
55
  ) -> None:
50
56
  self.instrument = instrument
51
57
  self.time_service = time_provider
@@ -57,6 +63,7 @@ class OrdersManagementEngine:
57
63
  self.bbo = None
58
64
  self.__order_id = 100000
59
65
  self.__trade_id = 100000
66
+ self._fill_stops_at_price = fill_stop_order_at_price
60
67
  if not debug:
61
68
  self._dbg = lambda message, **kwargs: None
62
69
 
@@ -96,12 +103,14 @@ class OrdersManagementEngine:
96
103
  # - processing stop orders
97
104
  for soid in list(self.stop_orders.keys()):
98
105
  so = self.stop_orders[soid]
106
+ _emulate_price_exec = self._fill_stops_at_price or so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
107
+
99
108
  if so.side == "BUY" and quote.ask >= so.price:
100
- _exec_price = quote.ask if not so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False) else so.price
109
+ _exec_price = quote.ask if not _emulate_price_exec else so.price
101
110
  self.stop_orders.pop(soid)
102
111
  rep.append(self._execute_order(timestamp, _exec_price, so, True))
103
112
  elif so.side == "SELL" and quote.bid <= so.price:
104
- _exec_price = quote.bid if not so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False) else so.price
113
+ _exec_price = quote.bid if not _emulate_price_exec else so.price
105
114
  self.stop_orders.pop(soid)
106
115
  rep.append(self._execute_order(timestamp, _exec_price, so, True))
107
116
 
@@ -142,8 +142,8 @@ class SimulatedTrading(ITradingServiceProvider):
142
142
  First implementation of a simulated broker.
143
143
  TODO:
144
144
  1. Add margin control
145
- 2. Need to solve problem with _get_ohlcv_data_sync (actually this method must be removed from here)
146
- 3. Add support for stop orders (not urgent)
145
+ 2. Need to solve problem with _get_ohlcv_data_sync (actually this method must be removed from here) [DONE]
146
+ 3. Add support for stop orders (not urgent) [DONE]
147
147
  """
148
148
 
149
149
  _current_time: dt_64
@@ -152,13 +152,40 @@ class SimulatedTrading(ITradingServiceProvider):
152
152
  _fees_calculator: TransactionCostsCalculator | None
153
153
  _order_to_symbol: Dict[str, str]
154
154
  _half_tick_size: Dict[str, float]
155
+ _fill_stop_order_at_price: bool
155
156
 
156
157
  def __init__(
157
158
  self,
158
159
  name: str,
159
160
  commissions: str | None = None,
160
161
  simulation_initial_time: dt_64 | str = np.datetime64(0, "ns"),
162
+ accurate_stop_orders_execution: bool = False,
161
163
  ) -> None:
164
+ """
165
+ This function sets up a simulated trading environment with following parameters.
166
+
167
+ Parameters:
168
+ -----------
169
+ name : str
170
+ The name of the simulated trading environment.
171
+ commissions : str | None, optional
172
+ The commission structure to be used. If None, no commissions will be applied.
173
+ Default is None.
174
+ simulation_initial_time : dt_64 | str, optional
175
+ The initial time for the simulation. Can be a dt_64 object or a string.
176
+ Default is np.datetime64(0, "ns").
177
+ accurate_stop_orders_execution : bool, optional
178
+ If True, stop orders will be executed at the exact stop order's price.
179
+ If False, they may be executed at the next quote that could lead to
180
+ significant slippage especially if simuation run on OHLC data.
181
+ Default is False.
182
+
183
+ Raises:
184
+ -------
185
+ ValueError
186
+ If the fees configuration is not found for the given name.
187
+
188
+ """
162
189
  self._current_time = (
163
190
  np.datetime64(simulation_initial_time, "ns")
164
191
  if isinstance(simulation_initial_time, str)
@@ -168,6 +195,7 @@ class SimulatedTrading(ITradingServiceProvider):
168
195
  self._ome = {}
169
196
  self._fees_calculator = lookup.fees.find(name.lower(), commissions)
170
197
  self._half_tick_size = {}
198
+ self._fill_stop_order_at_price = accurate_stop_orders_execution
171
199
 
172
200
  self._order_to_symbol = {}
173
201
  if self._fees_calculator is None:
@@ -177,6 +205,8 @@ class SimulatedTrading(ITradingServiceProvider):
177
205
 
178
206
  # - we want to see simulate time in log messages
179
207
  QubxLogConfig.setup_logger(QubxLogConfig.get_log_level(), _SimulatedLogFormatter(self).formatter)
208
+ if self._fill_stop_order_at_price:
209
+ logger.info(f"SimulatedExchangeService emulates stop orders executions at exact price")
180
210
 
181
211
  def send_order(
182
212
  self,
@@ -195,8 +225,8 @@ class SimulatedTrading(ITradingServiceProvider):
195
225
 
196
226
  # - try to place order in OME
197
227
  report = ome.place_order(
198
- order_side.upper(),
199
- order_type.upper(),
228
+ order_side.upper(), # type: ignore
229
+ order_type.upper(), # type: ignore
200
230
  amount,
201
231
  price,
202
232
  client_id,
@@ -254,7 +284,12 @@ class SimulatedTrading(ITradingServiceProvider):
254
284
 
255
285
  if symbol not in self.acc._positions:
256
286
  # - initiolize OME for this instrument
257
- self._ome[instrument.symbol] = OrdersManagementEngine(instrument=instrument, time_provider=self, tcc=self._fees_calculator) # type: ignore
287
+ self._ome[instrument.symbol] = OrdersManagementEngine(
288
+ instrument=instrument,
289
+ time_provider=self,
290
+ tcc=self._fees_calculator, # type: ignore
291
+ fill_stop_order_at_price=self._fill_stop_order_at_price,
292
+ )
258
293
 
259
294
  # - initiolize empty position
260
295
  position = Position(instrument) # type: ignore
@@ -693,8 +728,57 @@ def simulate(
693
728
  n_jobs: int = 1,
694
729
  silent: bool = False,
695
730
  enable_event_batching: bool = True,
731
+ accurate_stop_orders_execution: bool = False,
696
732
  debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
697
733
  ) -> list[TradingSessionResult]:
734
+ """
735
+ Backtest utility for trading strategies or signals using historical data.
736
+
737
+ Parameters:
738
+ ----------
739
+
740
+ config (StrategyOrSignals | Dict | List[StrategyOrSignals | PositionsTracker]):
741
+ Trading strategy or signals configuration.
742
+ data (Dict[str, pd.DataFrame] | DataReader):
743
+ Historical data for simulation, either as a dictionary of DataFrames or a DataReader object.
744
+ capital (float):
745
+ Initial capital for the simulation.
746
+ instruments (List[str] | Dict[str, List[str]] | None):
747
+ List of trading instruments or a dictionary mapping exchanges to instrument lists.
748
+ subscription (Dict[str, Any]):
749
+ Subscription details for market data.
750
+ trigger (str | list[str]):
751
+ Trigger specification for strategy execution.
752
+ commissions (str):
753
+ Commission structure for trades.
754
+ start (str | pd.Timestamp):
755
+ Start time of the simulation.
756
+ stop (str | pd.Timestamp | None):
757
+ End time of the simulation. If None, simulates until the last accessible data.
758
+ fit (str | None):
759
+ Specification for strategy fitting, if applicable.
760
+ exchange (str | None):
761
+ Exchange name if not specified in the instruments list.
762
+ base_currency (str):
763
+ Base currency for the simulation, default is "USDT".
764
+ leverage (float):
765
+ Leverage factor for trading, default is 1.0.
766
+ n_jobs (int):
767
+ Number of parallel jobs for simulation, default is 1.
768
+ silent (bool):
769
+ If True, suppresses output during simulation.
770
+ enable_event_batching (bool):
771
+ If True, enables event batching for optimization.
772
+ accurate_stop_orders_execution (bool):
773
+ If True, enables more accurate stop order execution simulation.
774
+ debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None):
775
+ Logging level for debugging.
776
+
777
+ Returns:
778
+ --------
779
+ list[TradingSessionResult]:
780
+ A list of TradingSessionResult objects containing the results of each simulation setup.
781
+ """
698
782
 
699
783
  # - setup logging
700
784
  QubxLogConfig.set_log_level(debug.upper() if debug else "WARNING")
@@ -755,6 +839,7 @@ def simulate(
755
839
  n_jobs=n_jobs,
756
840
  silent=silent,
757
841
  enable_event_batching=enable_event_batching,
842
+ accurate_stop_orders_execution=accurate_stop_orders_execution,
758
843
  )
759
844
 
760
845
 
@@ -818,6 +903,7 @@ def _run_setups(
818
903
  n_jobs: int = -1,
819
904
  silent: bool = False,
820
905
  enable_event_batching: bool = True,
906
+ accurate_stop_orders_execution: bool = False,
821
907
  ) -> List[TradingSessionResult]:
822
908
  # loggers don't work well with joblib and multiprocessing in general because they contain
823
909
  # open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
@@ -839,6 +925,7 @@ def _run_setups(
839
925
  fit=fit,
840
926
  silent=silent,
841
927
  enable_event_batching=enable_event_batching,
928
+ accurate_stop_orders_execution=accurate_stop_orders_execution,
842
929
  )
843
930
  for id, s in enumerate(setups)
844
931
  )
@@ -856,13 +943,19 @@ def _run_setup(
856
943
  fit: str | None,
857
944
  silent: bool = False,
858
945
  enable_event_batching: bool = True,
946
+ accurate_stop_orders_execution: bool = False,
859
947
  ) -> TradingSessionResult:
860
948
  _trigger = trigger
861
949
  _stop = stop
862
950
  logger.debug(
863
951
  f"<red>{pd.Timestamp(start)}</red> Initiating simulated trading for {setup.exchange} for {setup.capital} x {setup.leverage} in {setup.base_currency}..."
864
952
  )
865
- broker = SimulatedTrading(setup.exchange, setup.commissions, np.datetime64(start, "ns"))
953
+ broker = SimulatedTrading(
954
+ setup.exchange,
955
+ setup.commissions,
956
+ np.datetime64(start, "ns"),
957
+ accurate_stop_orders_execution=accurate_stop_orders_execution,
958
+ )
866
959
  exchange = SimulatedExchange(setup.exchange, broker, data_reader)
867
960
 
868
961
  # - it will store simulation results into memory
@@ -4,12 +4,16 @@ from typing import Any, Tuple
4
4
  import pandas as pd
5
5
 
6
6
  class Bar:
7
+ time: int
7
8
  open: float
8
9
  high: float
9
10
  low: float
10
11
  close: float
11
12
  volume: float
13
+ bought_volume: float
12
14
  def __init__(self, time, open, high, low, close, volume, bought_volume=0): ...
15
+ def update(self, price: float, volume: float, bought_volume: float = 0) -> Bar: ...
16
+ def to_dict(self, skip_time: bool = False) -> dict: ...
13
17
 
14
18
  class Quote:
15
19
  time: int
@@ -44,8 +48,9 @@ class TimeSeries:
44
48
  max_series_length: int
45
49
  times: Indexed
46
50
  values: Indexed
47
- def __init__(self, name, timeframe, max_series_length, process_every_update=True) -> None: ...
51
+ def __init__(self, name, timeframe, max_series_length=np.inf, process_every_update=True) -> None: ...
48
52
  def __getitem__(self, idx): ...
53
+ def __len__(self) -> int: ...
49
54
  def update(self, time: int, value: float) -> bool: ...
50
55
  def copy(self, start: int, stop: int) -> "TimeSeries": ...
51
56
  def shift(self, period: int) -> TimeSeries: ...
@@ -34,7 +34,7 @@ class SimplePositionGatherer(IPositionGathering):
34
34
 
35
35
  if abs(to_trade) < instrument.min_size:
36
36
  if current_position != 0:
37
- logger.warning(
37
+ logger.debug(
38
38
  f"{instrument.exchange}:{instrument.symbol}: Unable change position from {current_position} to {new_size} : too small difference"
39
39
  )
40
40
  else:
@@ -45,7 +45,7 @@ class SimplePositionGatherer(IPositionGathering):
45
45
  if at_price:
46
46
  # - we already havbe position but it's requested to change at a specific price
47
47
  if abs(current_position) > instrument.min_size:
48
- logger.warning(
48
+ logger.debug(
49
49
  f"<green>{instrument.symbol}</green>: Attempt to change current position {current_position} to {new_size} at {at_price} !"
50
50
  )
51
51
 
@@ -201,6 +201,13 @@ class BrokerSideRiskController(RiskController):
201
201
 
202
202
  case State.RISK_TRIGGERED:
203
203
  c.status = State.DONE
204
+
205
+ # - remove from the tracking list
206
+ logger.debug(
207
+ f"<yellow>{self.__class__.__name__}</yellow> -- stops tracking -- <green>{instrument.symbol}</green>"
208
+ )
209
+ self._trackings.pop(instrument)
210
+
204
211
  # - send service signal that risk triggeres (it won't be processed by StrategyContext)
205
212
  if c.stop_executed_price:
206
213
  return [
@@ -215,9 +222,6 @@ class BrokerSideRiskController(RiskController):
215
222
  )
216
223
  ]
217
224
 
218
- case State.OPEN:
219
- pass
220
-
221
225
  case State.DONE:
222
226
  logger.debug(
223
227
  f"<yellow>{self.__class__.__name__}</yellow> -- stops tracking -- <green>{instrument.symbol}</green>"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes