Qubx 0.5.1__tar.gz → 0.5.5__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 (99) hide show
  1. {qubx-0.5.1 → qubx-0.5.5}/PKG-INFO +2 -1
  2. {qubx-0.5.1 → qubx-0.5.5}/pyproject.toml +3 -2
  3. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/__init__.py +2 -2
  4. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/_nb_magic.py +1 -0
  5. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/__init__.py +2 -1
  6. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/account.py +5 -7
  7. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/data.py +58 -38
  8. qubx-0.5.5/src/qubx/backtester/management.py +141 -0
  9. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/ome.py +8 -11
  10. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/optimization.py +19 -12
  11. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/simulated_data.py +115 -73
  12. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/simulator.py +137 -126
  13. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/utils.py +105 -16
  14. qubx-0.5.5/src/qubx/cli/commands.py +67 -0
  15. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/customizations.py +1 -3
  16. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/account.py +6 -4
  17. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/basics.py +18 -10
  18. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/context.py +23 -19
  19. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/exceptions.py +2 -2
  20. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/helpers.py +38 -17
  21. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/interfaces.py +74 -8
  22. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/loggers.py +1 -0
  23. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/lookups.py +98 -17
  24. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/metrics.py +129 -30
  25. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/market.py +11 -4
  26. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/processing.py +25 -27
  27. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/subscription.py +14 -14
  28. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/trading.py +3 -2
  29. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/universe.py +2 -5
  30. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/series.pyi +1 -0
  31. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/series.pyx +13 -0
  32. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/gathering/simplest.py +5 -6
  33. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/math/stats.py +29 -6
  34. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/pandaz/ta.py +6 -9
  35. qubx-0.5.5/src/qubx/resources/instruments/symbols-binance.cm.json +1 -0
  36. qubx-0.5.5/src/qubx/resources/instruments/symbols-binance.json +1 -0
  37. qubx-0.5.5/src/qubx/resources/instruments/symbols-binance.um.json +1 -0
  38. qubx-0.5.5/src/qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  39. qubx-0.5.5/src/qubx/resources/instruments/symbols-bitfinex.json +1 -0
  40. qubx-0.5.5/src/qubx/resources/instruments/symbols-kraken.f.json +1 -0
  41. qubx-0.5.5/src/qubx/resources/instruments/symbols-kraken.json +1 -0
  42. qubx-0.5.5/src/qubx/trackers/abvanced.py +236 -0
  43. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/composite.py +2 -2
  44. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/riskctrl.py +126 -80
  45. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/sizers.py +71 -27
  46. qubx-0.5.5/src/qubx/utils/__init__.py +5 -0
  47. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/charting/lookinglass.py +36 -75
  48. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/charting/mpl_helpers.py +26 -12
  49. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/marketdata/ccxt.py +3 -1
  50. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/misc.py +77 -15
  51. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/orderbook.py +9 -9
  52. {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/dashboard.py +1 -2
  53. qubx-0.5.5/src/qubx/utils/runner/__init__.py +1 -0
  54. {qubx-0.5.1/src/qubx/utils → qubx-0.5.5/src/qubx/utils/runner}/_jupyter_runner.pyt +4 -3
  55. qubx-0.5.5/src/qubx/utils/runner/accounts.py +88 -0
  56. qubx-0.5.5/src/qubx/utils/runner/configs.py +65 -0
  57. qubx-0.5.5/src/qubx/utils/runner/runner.py +459 -0
  58. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/time.py +15 -11
  59. qubx-0.5.1/src/qubx/utils/__init__.py +0 -4
  60. qubx-0.5.1/src/qubx/utils/runner.py +0 -543
  61. {qubx-0.5.1 → qubx-0.5.5}/README.md +0 -0
  62. {qubx-0.5.1 → qubx-0.5.5}/build.py +0 -0
  63. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/broker.py +0 -0
  64. {qubx-0.5.1/src/qubx/connectors/ccxt → qubx-0.5.5/src/qubx/cli}/__init__.py +0 -0
  65. {qubx-0.5.1/src/qubx/core → qubx-0.5.5/src/qubx/connectors/ccxt}/__init__.py +0 -0
  66. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/account.py +0 -0
  67. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/broker.py +0 -0
  68. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/data.py +0 -0
  69. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  70. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/factory.py +0 -0
  71. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/utils.py +0 -0
  72. {qubx-0.5.1/src/qubx/plotting → qubx-0.5.5/src/qubx/core}/__init__.py +0 -0
  73. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/__init__.py +0 -0
  74. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/series.pxd +0 -0
  75. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/utils.pyi +0 -0
  76. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/utils.pyx +0 -0
  77. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/__init__.py +0 -0
  78. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/helpers.py +0 -0
  79. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/readers.py +0 -0
  80. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/tardis.py +0 -0
  81. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/math/__init__.py +0 -0
  82. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/pandaz/__init__.py +0 -0
  83. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/pandaz/utils.py +0 -0
  84. {qubx-0.5.1/src/qubx/plotting/renderers → qubx-0.5.5/src/qubx/ta}/__init__.py +0 -0
  85. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/ta/indicators.pxd +0 -0
  86. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/ta/indicators.pyi +0 -0
  87. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/ta/indicators.pyx +0 -0
  88. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/__init__.py +0 -0
  89. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/rebalancers.py +0 -0
  90. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/_pyxreloader.py +0 -0
  91. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/marketdata/binance.py +0 -0
  92. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/marketdata/dukas.py +0 -0
  93. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/ntp.py +0 -0
  94. {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/numbers_utils.py +0 -0
  95. {qubx-0.5.1/src/qubx/ta → qubx-0.5.5/src/qubx/utils/plotting}/__init__.py +0 -0
  96. {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/data.py +0 -0
  97. {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/interfaces.py +0 -0
  98. /qubx-0.5.1/src/qubx/connectors/ccxt/ccxt_connector.py → /qubx-0.5.5/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  99. {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/renderers/plotly.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.5.1
3
+ Version: 0.5.5
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -40,6 +40,7 @@ Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
40
40
  Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
41
41
  Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
42
42
  Requires-Dist: tabulate (>=0.9.0,<0.10.0)
43
+ Requires-Dist: toml (>=0.10.2,<0.11.0)
43
44
  Requires-Dist: tqdm
44
45
  Project-URL: Repository, https://github.com/dmarienko/Qubx
45
46
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.5.1"
3
+ version = "0.5.5"
4
4
  description = "Qubx - quantitative trading framework"
5
5
  authors = [
6
6
  "Dmitry Marienko <dmitry@gmail.com>",
@@ -48,6 +48,7 @@ dash = "^2.18.2"
48
48
  dash-bootstrap-components = "^1.6.0"
49
49
  tabulate = "^0.9.0"
50
50
  jupyter-console = "^6.6.3"
51
+ toml = "^0.10.2"
51
52
 
52
53
  [tool.poetry.group.dev.dependencies]
53
54
  pre-commit = "^2.20.0"
@@ -95,4 +96,4 @@ line-length = 120
95
96
  "*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
96
97
 
97
98
  [tool.poetry.scripts]
98
- qubx = "qubx.utils.runner:main"
99
+ qubx = "qubx.cli.commands:main"
@@ -37,7 +37,7 @@ def formatter(record):
37
37
  class QubxLogConfig:
38
38
  @staticmethod
39
39
  def get_log_level():
40
- return os.getenv("QUBX_LOG_LEVEL", "DEBUG")
40
+ return os.getenv("QUBX_LOG_LEVEL", "WARNING")
41
41
 
42
42
  @staticmethod
43
43
  def set_log_level(level: str):
@@ -115,7 +115,7 @@ if runtime_env() in ["notebook", "shell"]:
115
115
  # - temporary workaround for vscode - dark theme not applying to ipywidgets in notebook
116
116
  # - see https://github.com/microsoft/vscode-jupyter/issues/7161
117
117
  if runtime_env() == "notebook":
118
- _vscode_clr_trick = """from IPython.display import display, HTML; display(HTML("<style> .cell-output-ipywidget-background { background-color: transparent !important; } :root { --jp-widgets-color: var(--vscode-editor-foreground); --jp-widgets-font-size: var(--vscode-editor-font-size); } </style>"))"""
118
+ _vscode_clr_trick = """from IPython.display import display, HTML; display(HTML("<style> .cell-output-ipywidget-background { background-color: transparent !important; } :root { --jp-widgets-color: var(--vscode-editor-foreground); --jp-widgets-font-size: var(--vscode-editor-font-size); } .widget-hprogress, .jupyter-widget-hprogress { height: 16px; align-self: center; kj} table.dataframe, .dataframe td, .dataframe tr { border: 1px solid #55554a85; border-collapse: collapse; color: #859548d9 !important; } .dataframe th { border: 1px solid #55554a85; border-collapse: collapse; background-color: #010101 !important; color: #177 !important; } </style>"))"""
119
119
  exec(_vscode_clr_trick, self.shell.user_ns)
120
120
 
121
121
  elif "light" in line.lower():
@@ -86,6 +86,7 @@ if runtime_env() in ["notebook", "shell"]:
86
86
  )
87
87
  from qubx.utils.charting.lookinglass import LookingGlass
88
88
  from qubx.utils.charting.mpl_helpers import fig, ohlc_plot, plot_trends, sbp, subplot
89
+ from qubx.utils.misc import this_project_root
89
90
 
90
91
  # - setup short numpy output format
91
92
  np_fmt_short()
@@ -1,2 +1,3 @@
1
- from .simulator import simulate
1
+ from .management import BacktestsResultsManager
2
2
  from .optimization import variate
3
+ from .simulator import simulate
@@ -3,7 +3,6 @@ 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,
7
6
  CtrlChannel,
8
7
  Instrument,
9
8
  Order,
@@ -47,7 +46,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
47
46
  self._half_tick_size = {}
48
47
  self._fill_stop_order_at_price = accurate_stop_orders_execution
49
48
  if self._fill_stop_order_at_price:
50
- logger.info(f"{self.__class__.__name__} emulates stop orders executions at exact price")
49
+ logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
51
50
 
52
51
  def get_orders(self, instrument: Instrument | None = None) -> list[Order]:
53
52
  if instrument is not None:
@@ -103,31 +102,30 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
103
102
  return super().process_order(order, update_locked_value)
104
103
 
105
104
  def emulate_quote_from_data(
106
- self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped | BatchEvent
105
+ self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
107
106
  ) -> Quote | None:
108
107
  if instrument not in self._half_tick_size:
109
108
  _ = self.get_position(instrument)
110
109
 
111
- _ts2 = self._half_tick_size[instrument]
112
110
  if isinstance(data, Quote):
113
111
  return data
114
112
 
115
113
  elif isinstance(data, Trade):
114
+ _ts2 = self._half_tick_size[instrument]
116
115
  if data.taker: # type: ignore
117
116
  return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
118
117
  else:
119
118
  return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
120
119
 
121
120
  elif isinstance(data, Bar):
121
+ _ts2 = self._half_tick_size[instrument]
122
122
  return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
123
123
 
124
124
  elif isinstance(data, OrderBook):
125
125
  return data.to_quote()
126
126
 
127
- elif isinstance(data, BatchEvent):
128
- return self.emulate_quote_from_data(instrument, timestamp, data.data[-1])
129
-
130
127
  elif isinstance(data, float):
128
+ _ts2 = self._half_tick_size[instrument]
131
129
  return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
132
130
 
133
131
  else:
@@ -6,13 +6,14 @@ import pandas as pd
6
6
  from tqdm.auto import tqdm
7
7
 
8
8
  from qubx import logger
9
- from qubx.backtester.simulated_data import EventBatcher, IterableSimulationData
9
+ from qubx.backtester.simulated_data import IterableSimulationData
10
10
  from qubx.core.basics import (
11
11
  CtrlChannel,
12
12
  DataType,
13
13
  Instrument,
14
14
  TimestampedDict,
15
15
  )
16
+ from qubx.core.exceptions import SimulationError
16
17
  from qubx.core.helpers import BasicScheduler
17
18
  from qubx.core.interfaces import IDataProvider
18
19
  from qubx.core.series import Bar, Quote, time_as_nsec
@@ -74,23 +75,22 @@ class SimulatedDataProvider(IDataProvider):
74
75
  start: str | pd.Timestamp,
75
76
  end: str | pd.Timestamp,
76
77
  silent: bool = False,
77
- enable_event_batching: bool = True,
78
78
  ) -> None:
79
79
  logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
80
80
 
81
81
  if self._pregenerated_signals:
82
82
  self._prepare_generated_signals(start, end)
83
- _run = self._run_generated_signals
84
- enable_event_batching = False # no batching for pre-generated signals
83
+ _run = self._process_generated_signals
85
84
  else:
86
- _run = self._run_as_strategy
85
+ _run = self._process_strategy
87
86
 
88
- qiter = EventBatcher(self._data_source.create_iterable(start, end), passthrough=not enable_event_batching)
89
87
  start, end = pd.Timestamp(start), pd.Timestamp(end)
90
88
  total_duration = end - start
91
89
  update_delta = total_duration / 100
92
90
  prev_dt = pd.Timestamp(start)
93
91
 
92
+ # - date iteration
93
+ qiter = self._data_source.create_iterable(start, end)
94
94
  if silent:
95
95
  for instrument, data_type, event, is_hist in qiter:
96
96
  if not _run(instrument, data_type, event, is_hist):
@@ -114,7 +114,9 @@ class SimulatedDataProvider(IDataProvider):
114
114
  logger.info(f"{self.__class__.__name__} ::: Simulation finished at {end} :::")
115
115
 
116
116
  def set_generated_signals(self, signals: pd.Series | pd.DataFrame):
117
- logger.debug(f"Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}")
117
+ logger.debug(
118
+ f"[<y>{self.__class__.__name__}</y>] :: Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}"
119
+ )
118
120
  # - sanity check
119
121
  signals.index = pd.DatetimeIndex(signals.index)
120
122
 
@@ -132,11 +134,29 @@ class SimulatedDataProvider(IDataProvider):
132
134
  return True
133
135
 
134
136
  def subscribe(self, subscription_type: str, instruments: set[Instrument], reset: bool) -> None:
135
- logger.debug(f" | subscribe: {subscription_type} -> {instruments}")
137
+ _new_instr = [i for i in instruments if not self.has_subscription(i, subscription_type)]
136
138
  self._data_source.add_instruments_for_subscription(subscription_type, list(instruments))
137
139
 
140
+ # - provide historical data and last quote for subscribed instruments
141
+ for i in _new_instr:
142
+ h_data = self._data_source.peek_historical_data(i, subscription_type)
143
+ if h_data:
144
+ # _s_type = DataType.from_str(subscription_type)[0]
145
+ last_update = h_data[-1]
146
+ if last_quote := self._account.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
147
+ # - send historical data to the channel
148
+ self.channel.send((i, subscription_type, h_data, True))
149
+
150
+ # - set last quote
151
+ self._last_quotes[i] = last_quote
152
+
153
+ # - also need to pass this quote to OME !
154
+ self._account._process_new_quote(i, last_quote)
155
+
156
+ logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
157
+
138
158
  def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
139
- logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
159
+ # logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
140
160
  if instruments is not None:
141
161
  self._data_source.remove_instruments_from_subscription(
142
162
  subscription_type, [instruments] if isinstance(instruments, Instrument) else list(instruments)
@@ -147,12 +167,12 @@ class SimulatedDataProvider(IDataProvider):
147
167
 
148
168
  def get_subscriptions(self, instrument: Instrument) -> list[str]:
149
169
  _s_lst = self._data_source.get_subscriptions_for_instrument(instrument)
150
- logger.debug(f" | get_subscriptions {instrument} -> {_s_lst}")
170
+ # logger.debug(f" | get_subscriptions {instrument} -> {_s_lst}")
151
171
  return _s_lst
152
172
 
153
173
  def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
154
174
  _in_lst = self._data_source.get_instruments_for_subscription(subscription_type or DataType.ALL)
155
- logger.debug(f" | get_subscribed_instruments {subscription_type} -> {_in_lst}")
175
+ # logger.debug(f" | get_subscribed_instruments {subscription_type} -> {_in_lst}")
156
176
  return _in_lst
157
177
 
158
178
  def warmup(self, configs: dict[tuple[str, Instrument], str]) -> None:
@@ -190,15 +210,16 @@ class SimulatedDataProvider(IDataProvider):
190
210
  if s == i.symbol or s == str(i) or s == f"{i.exchange}:{i.symbol}" or str(s) == str(i):
191
211
  _start, _end = pd.Timestamp(start), pd.Timestamp(end)
192
212
  _start_idx, _end_idx = v.index.get_indexer([_start, _end], method="ffill")
193
- sel = v.iloc[max(_start_idx, 0) : _end_idx + 1] # sel = v[pd.Timestamp(start) : pd.Timestamp(end)]
213
+ sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
194
214
 
215
+ # TODO: check if data has exec_price - it means we have deals
195
216
  self._to_process[i] = list(zip(sel.index, sel.values))
196
217
  _s_inst = i
197
218
  break
198
219
 
199
220
  if _s_inst is None:
200
221
  logger.error(f"Can't find instrument for pregenerated signals with id '{s}'")
201
- raise ValueError(f"Can't find instrument for pregenerated signals with id '{s}'")
222
+ raise SimulationError(f"Can't find instrument for pregenerated signals with id '{s}'")
202
223
 
203
224
  def _convert_records_to_bars(
204
225
  self, records: list[TimestampedDict], cut_time_ns: int, timeframe_ns: int
@@ -228,42 +249,41 @@ class SimulatedDataProvider(IDataProvider):
228
249
 
229
250
  return bars
230
251
 
231
- def _run_generated_signals(self, instrument: Instrument, data_type: str, data: Any, is_hist) -> bool:
232
- if is_hist:
233
- raise ValueError("Historical data is not supported for pre-generated signals !")
252
+ def _process_generated_signals(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
253
+ cc = self.channel
254
+ t = np.datetime64(data.time, "ns")
234
255
 
235
- t = data.time # type: ignore
236
- self.time_provider.set_time(np.datetime64(t, "ns"))
256
+ if not is_hist:
257
+ # - signals for this instrument
258
+ sigs = self._to_process[instrument]
237
259
 
238
- q = self._account.emulate_quote_from_data(instrument, np.datetime64(t, "ns"), data)
239
- self._last_quotes[instrument] = q
240
- cc = self.channel
260
+ while sigs and t >= (_signal_time := sigs[0][0].as_unit("ns").asm8):
261
+ self.time_provider.set_time(_signal_time)
262
+ cc.send((instrument, "event", {"order": sigs[0][1]}, False))
263
+ sigs.pop(0)
241
264
 
242
- # - we need to send quotes for invoking portfolio logging etc
265
+ if q := self._account.emulate_quote_from_data(instrument, t, data):
266
+ self._last_quotes[instrument] = q
267
+
268
+ self.time_provider.set_time(t)
243
269
  cc.send((instrument, data_type, data, is_hist))
244
- sigs = self._to_process[instrument]
245
- _current_time = self.time_provider.time()
246
- while sigs and sigs[0][0].as_unit("ns").asm8 <= _current_time:
247
- cc.send((instrument, "event", {"order": sigs[0][1]}, is_hist))
248
- sigs.pop(0)
249
270
 
250
271
  return cc.control.is_set()
251
272
 
252
- def _run_as_strategy(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
253
- t = data.time # type: ignore
254
- self.time_provider.set_time(np.datetime64(t, "ns"))
255
-
256
- q = self._account.emulate_quote_from_data(instrument, np.datetime64(t, "ns"), data)
273
+ def _process_strategy(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
257
274
  cc = self.channel
275
+ t = np.datetime64(data.time, "ns")
258
276
 
259
- if not is_hist and q is not None:
260
- self._last_quotes[instrument] = q
277
+ if not is_hist:
278
+ if t >= (_next_exp_time := self._scheduler.next_expected_event_time()):
279
+ # - we use exact event's time
280
+ self.time_provider.set_time(_next_exp_time)
281
+ self._scheduler.check_and_run_tasks()
261
282
 
262
- # we have to schedule possible crons before sending the data event itself
263
- if self._scheduler.check_and_run_tasks():
264
- # - push nothing - it will force to process last event
265
- cc.send((None, "service_time", None, False))
283
+ if q := self._account.emulate_quote_from_data(instrument, t, data):
284
+ self._last_quotes[instrument] = q
266
285
 
286
+ self.time_provider.set_time(t)
267
287
  cc.send((instrument, data_type, data, is_hist))
268
288
 
269
289
  return cc.control.is_set()
@@ -0,0 +1,141 @@
1
+ import re
2
+ import zipfile
3
+ from collections import defaultdict
4
+ from pathlib import Path
5
+
6
+ import pandas as pd
7
+ import yaml
8
+
9
+ from qubx.core.metrics import TradingSessionResult, _pfl_metrics_prepare
10
+ from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
11
+
12
+
13
+ class BacktestsResultsManager:
14
+ """
15
+ Manager class for handling backtesting results.
16
+
17
+ This class provides functionality to load, list and manage backtesting results stored in zip files.
18
+ Each result contains trading session information and metrics that can be loaded and analyzed.
19
+
20
+ Parameters
21
+ ----------
22
+ path : str
23
+ Path to directory containing backtesting result zip files
24
+
25
+ Methods
26
+ -------
27
+ reload()
28
+ Reloads all backtesting results from the specified path
29
+ list(regex="", with_metrics=False)
30
+ Lists all backtesting results, optionally filtered by regex and including metrics
31
+ load(name)
32
+ Loads a specific backtesting result by name
33
+ """
34
+
35
+ def __init__(self, path: str):
36
+ self.path = path
37
+ self.reload()
38
+
39
+ def reload(self) -> "BacktestsResultsManager":
40
+ self.results = {}
41
+ names = defaultdict(lambda: 0)
42
+ for p in Path(self.path).glob("**/*.zip"):
43
+ with zipfile.ZipFile(p, "r") as zip_ref:
44
+ try:
45
+ info = yaml.safe_load(zip_ref.read("info.yml"))
46
+ info["path"] = str(p)
47
+ n = info.get("name", "")
48
+ _new_name = n if names[n] == 0 else f"{n}.{names[n]}"
49
+ names[n] += 1
50
+ info["name"] = _new_name
51
+ self.results[_new_name] = info
52
+ except Exception:
53
+ pass
54
+
55
+ # - reindex
56
+ _idx = 1
57
+ for n in sorted(self.results.keys()):
58
+ self.results[n]["idx"] = _idx
59
+ _idx += 1
60
+
61
+ return self
62
+
63
+ def load(self, name: str | int | list[int] | list[str]) -> TradingSessionResult | list[TradingSessionResult]:
64
+ for info in self.results.values():
65
+ match name:
66
+ case int():
67
+ if info.get("idx", -1) == name:
68
+ return TradingSessionResult.from_file(info["path"])
69
+ case str():
70
+ if info.get("name", "") == name:
71
+ return TradingSessionResult.from_file(info["path"])
72
+ case list():
73
+ return [self.load(i) for i in name]
74
+
75
+ raise ValueError(f"No result found for {name}")
76
+
77
+ def list(self, regex: str = "", with_metrics=False, params=False):
78
+ for n in sorted(self.results.keys()):
79
+ info = self.results[n]
80
+ s_cls = info.get("strategy_class", "").split(".")[-1]
81
+
82
+ if regex:
83
+ if not re.match(regex, n, re.IGNORECASE):
84
+ if not re.match(regex, s_cls, re.IGNORECASE):
85
+ continue
86
+
87
+ name = info.get("name", "")
88
+ smbs = ", ".join(info.get("symbols", list()))
89
+ start = pd.Timestamp(info.get("start", "")).round("1s")
90
+ stop = pd.Timestamp(info.get("stop", "")).round("1s")
91
+ dscr = info.get("description", "")
92
+ _s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(pd.Timestamp(info.get('creation_time', '')).round('1s'))} by {cyan(info.get('author', ''))}"
93
+
94
+ if dscr:
95
+ dscr = dscr.split("\n")
96
+ for _d in dscr:
97
+ _s += f"\n\t{magenta('# ' + _d)}"
98
+
99
+ _s += f"\n\tstrategy: {green(s_cls)}"
100
+ _s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
101
+ _s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
102
+ _s += f"\n\tinstruments: {blue(smbs)}"
103
+ if params:
104
+ formats = ["{" + f":<{i}" + "}" for i in [50]]
105
+ _p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
106
+ for i in _p.to_string(
107
+ max_colwidth=30,
108
+ header=False,
109
+ formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
110
+ justify="left",
111
+ ).split("\n"):
112
+ _s += f"\n\t | {yellow(i)}"
113
+ print(_s)
114
+
115
+ if with_metrics:
116
+ r = TradingSessionResult.from_file(info["path"])
117
+ metric = _pfl_metrics_prepare(r, True, 365)
118
+ _m_repr = str(metric[0][["Gain", "Cagr", "Sharpe", "Max dd pct", "Qr", "Fees"]].round(3)).split("\n")[
119
+ :-1
120
+ ]
121
+ for i in _m_repr:
122
+ print("\t " + cyan(i))
123
+ print()
124
+
125
+ def delete(self, name: str | int):
126
+ print(red(f" -> Danger zone - you are about to delete {name} ..."))
127
+ for info in self.results.values():
128
+ match name:
129
+ case int():
130
+ if info.get("idx", -1) == name:
131
+ Path(info["path"]).unlink()
132
+ print(f" -> Deleted {red(name)} ...")
133
+ self.reload()
134
+ return
135
+ case str():
136
+ if info.get("name", "") == name:
137
+ Path(info["path"]).unlink()
138
+ print(f" -> Deleted {red(name)} ...")
139
+ self.reload()
140
+ return
141
+ print(f" -> No results found for {red(name)} !")
@@ -1,6 +1,5 @@
1
1
  from dataclasses import dataclass
2
2
  from operator import neg
3
- from typing import Dict, List
4
3
 
5
4
  import numpy as np
6
5
  from sortedcontainers import SortedDict
@@ -14,8 +13,6 @@ from qubx.core.basics import (
14
13
  Order,
15
14
  OrderSide,
16
15
  OrderType,
17
- Position,
18
- Signal,
19
16
  TransactionCostsCalculator,
20
17
  dt_64,
21
18
  )
@@ -36,10 +33,10 @@ class OmeReport:
36
33
  class OrdersManagementEngine:
37
34
  instrument: Instrument
38
35
  time_service: ITimeProvider
39
- active_orders: Dict[str, Order]
40
- stop_orders: Dict[str, Order]
41
- asks: SortedDict[float, List[str]]
42
- bids: SortedDict[float, List[str]]
36
+ active_orders: dict[str, Order]
37
+ stop_orders: dict[str, Order]
38
+ asks: SortedDict[float, list[str]]
39
+ bids: SortedDict[float, list[str]]
43
40
  bbo: Quote | None # current best bid/ask order book (simplest impl)
44
41
  __order_id: int
45
42
  __trade_id: int
@@ -78,10 +75,10 @@ class OrdersManagementEngine:
78
75
  def get_quote(self) -> Quote:
79
76
  return self.bbo
80
77
 
81
- def get_open_orders(self) -> List[Order]:
78
+ def get_open_orders(self) -> list[Order]:
82
79
  return list(self.active_orders.values()) + list(self.stop_orders.values())
83
80
 
84
- def update_bbo(self, quote: Quote) -> List[OmeReport]:
81
+ def update_bbo(self, quote: Quote) -> list[OmeReport]:
85
82
  timestamp = self.time_service.time()
86
83
  rep = []
87
84
 
@@ -151,7 +148,7 @@ class OrdersManagementEngine:
151
148
  return self._process_order(timestamp, order)
152
149
 
153
150
  def _dbg(self, message, **kwargs) -> None:
154
- logger.debug(f"[OMS] {self.instrument.symbol} - {message}", **kwargs)
151
+ logger.debug(f" [<y>OME</y>(<g>{self.instrument}</g>)] :: {message}", **kwargs)
155
152
 
156
153
  def _process_order(self, timestamp: dt_64, order: Order) -> OmeReport:
157
154
  if order.status in ["CLOSED", "CANCELED"]:
@@ -179,7 +176,7 @@ class OrdersManagementEngine:
179
176
  self.stop_orders[order.id] = order
180
177
 
181
178
  elif order.type == "STOP_LIMIT":
182
- # TODO: check trigger conditions in options etc
179
+ # TODO: (OME) check trigger conditions in options etc
183
180
  raise NotImplementedError("'STOP_LIMIT' order is not supported in Qubx simulator yet !")
184
181
 
185
182
  # - if order must be "executed" immediately
@@ -1,28 +1,30 @@
1
1
  import re
2
2
  from itertools import product
3
3
  from types import FunctionType
4
- from typing import Any, Dict, List, Sequence, Tuple, Type
4
+ from typing import Any, Callable, Type
5
5
 
6
6
  import numpy as np
7
7
 
8
+ from qubx.utils.misc import generate_name
8
9
 
9
- def _wrap_single_list(param_grid: List | Dict) -> Dict[str, Any] | List:
10
+
11
+ def _wrap_single_list(param_grid: list | dict) -> dict[str, Any] | list:
10
12
  """
11
13
  Wraps all non list values as single
12
14
  :param param_grid:
13
15
  :return:
14
16
  """
15
- as_list = lambda x: x if isinstance(x, (tuple, list, dict, np.ndarray)) else [x]
17
+ as_list = lambda x: x if isinstance(x, (tuple, list, dict, np.ndarray)) else [x] # noqa: E731
16
18
  if isinstance(param_grid, list):
17
19
  return [_wrap_single_list(ps) for ps in param_grid]
18
20
  return {k: as_list(v) for k, v in param_grid.items()}
19
21
 
20
22
 
21
23
  def permutate_params(
22
- parameters: Dict[str, List | Tuple | Any],
23
- conditions: FunctionType | List | Tuple | None = None,
24
+ parameters: dict[str, list | tuple | Any],
25
+ conditions: FunctionType | list | tuple | None = None,
24
26
  wrap_as_list=False,
25
- ) -> List[Dict]:
27
+ ) -> list[dict]:
26
28
  """
27
29
  Generate list of all permutations for given parameters and theirs possible values
28
30
 
@@ -115,7 +117,7 @@ def dicts_product(d1: dict, d2: dict) -> dict:
115
117
  }
116
118
 
117
119
  """
118
- flatten = lambda l: [item for sublist in l for item in (sublist if isinstance(sublist, list) else [sublist])]
120
+ flatten = lambda l: [item for sublist in l for item in (sublist if isinstance(sublist, list) else [sublist])] # noqa: E731
119
121
  return {(a + " + " + b): flatten([d1[a], d2[b]]) for a, b in product(d1.keys(), d2.keys())}
120
122
 
121
123
 
@@ -124,7 +126,7 @@ class _dict(dict):
124
126
  return _dict(dicts_product(self, other))
125
127
 
126
128
 
127
- def variate(clz: Type[Any] | List[Type[Any]], *args, conditions=None, **kwargs) -> _dict:
129
+ def variate(clz: Type[Any] | list[Type[Any]], *args, conditions=None, **kwargs) -> _dict:
128
130
  """
129
131
  Make variations of parameters for simulations (micro optimizer)
130
132
 
@@ -169,19 +171,24 @@ def variate(clz: Type[Any] | List[Type[Any]], *args, conditions=None, **kwargs)
169
171
  def _cmprss(xs: str):
170
172
  return "".join([x[0] for x in re.split(r"((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
171
173
 
172
- if isinstance(clz, type):
174
+ if isinstance(clz, (type, Callable)):
173
175
  sfx = _cmprss(clz.__name__)
174
- _mk = lambda k, *args, **kwargs: k(*args, **kwargs)
176
+ _mk = lambda k, *args, **kwargs: k(*args, **kwargs) # noqa: E731
175
177
  elif isinstance(clz, (list, tuple)) and clz and isinstance(clz[0], type):
176
178
  sfx = _cmprss(clz[0].__name__)
177
- _mk = lambda k, *args, **kwargs: [k[0](*args, **kwargs), *k[1:]]
179
+ _mk = lambda k, *args, **kwargs: [k[0](*args, **kwargs), *k[1:]] # noqa: E731
178
180
  else:
179
181
  raise ValueError(
180
182
  "Can't recognize data for variating: must be either a class type or a list where first element is class type"
181
183
  )
182
184
 
185
+ def _v_to_str(x: Any) -> str:
186
+ if isinstance(x, (list, tuple, dict, set, np.ndarray)) and len(xs := str(x)) > 15:
187
+ return "[" + generate_name(xs, 8).lower() + "]"
188
+ return str(x)
189
+
183
190
  to_excl = [s for s, v in kwargs.items() if not isinstance(v, (list, set, tuple, range))]
184
- dic2str = lambda ds: [_cmprss(k) + "=" + str(v) for k, v in ds.items() if k not in to_excl]
191
+ dic2str = lambda ds: [_cmprss(k) + "=" + _v_to_str(v) for k, v in ds.items() if k not in to_excl] # noqa: E731
185
192
 
186
193
  return _dict(
187
194
  {