Qubx 0.5.7__cp312-cp312-manylinux_2_39_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.

Files changed (100) hide show
  1. qubx/__init__.py +207 -0
  2. qubx/_nb_magic.py +100 -0
  3. qubx/backtester/__init__.py +5 -0
  4. qubx/backtester/account.py +145 -0
  5. qubx/backtester/broker.py +87 -0
  6. qubx/backtester/data.py +296 -0
  7. qubx/backtester/management.py +378 -0
  8. qubx/backtester/ome.py +296 -0
  9. qubx/backtester/optimization.py +201 -0
  10. qubx/backtester/simulated_data.py +558 -0
  11. qubx/backtester/simulator.py +362 -0
  12. qubx/backtester/utils.py +780 -0
  13. qubx/cli/__init__.py +0 -0
  14. qubx/cli/commands.py +67 -0
  15. qubx/connectors/ccxt/__init__.py +0 -0
  16. qubx/connectors/ccxt/account.py +495 -0
  17. qubx/connectors/ccxt/broker.py +132 -0
  18. qubx/connectors/ccxt/customizations.py +193 -0
  19. qubx/connectors/ccxt/data.py +612 -0
  20. qubx/connectors/ccxt/exceptions.py +17 -0
  21. qubx/connectors/ccxt/factory.py +93 -0
  22. qubx/connectors/ccxt/utils.py +307 -0
  23. qubx/core/__init__.py +0 -0
  24. qubx/core/account.py +251 -0
  25. qubx/core/basics.py +850 -0
  26. qubx/core/context.py +420 -0
  27. qubx/core/exceptions.py +38 -0
  28. qubx/core/helpers.py +480 -0
  29. qubx/core/interfaces.py +1150 -0
  30. qubx/core/loggers.py +514 -0
  31. qubx/core/lookups.py +475 -0
  32. qubx/core/metrics.py +1512 -0
  33. qubx/core/mixins/__init__.py +13 -0
  34. qubx/core/mixins/market.py +94 -0
  35. qubx/core/mixins/processing.py +428 -0
  36. qubx/core/mixins/subscription.py +203 -0
  37. qubx/core/mixins/trading.py +88 -0
  38. qubx/core/mixins/universe.py +270 -0
  39. qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
  40. qubx/core/series.pxd +125 -0
  41. qubx/core/series.pyi +118 -0
  42. qubx/core/series.pyx +988 -0
  43. qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
  44. qubx/core/utils.pyi +6 -0
  45. qubx/core/utils.pyx +62 -0
  46. qubx/data/__init__.py +25 -0
  47. qubx/data/helpers.py +416 -0
  48. qubx/data/readers.py +1562 -0
  49. qubx/data/tardis.py +100 -0
  50. qubx/gathering/simplest.py +88 -0
  51. qubx/math/__init__.py +3 -0
  52. qubx/math/stats.py +129 -0
  53. qubx/pandaz/__init__.py +23 -0
  54. qubx/pandaz/ta.py +2757 -0
  55. qubx/pandaz/utils.py +638 -0
  56. qubx/resources/instruments/symbols-binance.cm.json +1 -0
  57. qubx/resources/instruments/symbols-binance.json +1 -0
  58. qubx/resources/instruments/symbols-binance.um.json +1 -0
  59. qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
  60. qubx/resources/instruments/symbols-bitfinex.json +1 -0
  61. qubx/resources/instruments/symbols-kraken.f.json +1 -0
  62. qubx/resources/instruments/symbols-kraken.json +1 -0
  63. qubx/ta/__init__.py +0 -0
  64. qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
  65. qubx/ta/indicators.pxd +149 -0
  66. qubx/ta/indicators.pyi +41 -0
  67. qubx/ta/indicators.pyx +787 -0
  68. qubx/trackers/__init__.py +3 -0
  69. qubx/trackers/abvanced.py +236 -0
  70. qubx/trackers/composite.py +146 -0
  71. qubx/trackers/rebalancers.py +129 -0
  72. qubx/trackers/riskctrl.py +641 -0
  73. qubx/trackers/sizers.py +235 -0
  74. qubx/utils/__init__.py +5 -0
  75. qubx/utils/_pyxreloader.py +281 -0
  76. qubx/utils/charting/lookinglass.py +1057 -0
  77. qubx/utils/charting/mpl_helpers.py +1183 -0
  78. qubx/utils/marketdata/binance.py +284 -0
  79. qubx/utils/marketdata/ccxt.py +90 -0
  80. qubx/utils/marketdata/dukas.py +130 -0
  81. qubx/utils/misc.py +541 -0
  82. qubx/utils/ntp.py +63 -0
  83. qubx/utils/numbers_utils.py +7 -0
  84. qubx/utils/orderbook.py +491 -0
  85. qubx/utils/plotting/__init__.py +0 -0
  86. qubx/utils/plotting/dashboard.py +150 -0
  87. qubx/utils/plotting/data.py +137 -0
  88. qubx/utils/plotting/interfaces.py +25 -0
  89. qubx/utils/plotting/renderers/__init__.py +0 -0
  90. qubx/utils/plotting/renderers/plotly.py +0 -0
  91. qubx/utils/runner/__init__.py +1 -0
  92. qubx/utils/runner/_jupyter_runner.pyt +60 -0
  93. qubx/utils/runner/accounts.py +88 -0
  94. qubx/utils/runner/configs.py +65 -0
  95. qubx/utils/runner/runner.py +470 -0
  96. qubx/utils/time.py +312 -0
  97. qubx-0.5.7.dist-info/METADATA +105 -0
  98. qubx-0.5.7.dist-info/RECORD +100 -0
  99. qubx-0.5.7.dist-info/WHEEL +4 -0
  100. qubx-0.5.7.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,362 @@
1
+ from typing import Literal
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from joblib import delayed
6
+
7
+ from qubx import QubxLogConfig, logger, lookup
8
+ from qubx.core.basics import SW, DataType
9
+ from qubx.core.context import StrategyContext
10
+ from qubx.core.exceptions import SimulationConfigError, SimulationError
11
+ from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
12
+ from qubx.core.interfaces import IStrategy
13
+ from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
14
+ from qubx.core.metrics import TradingSessionResult
15
+ from qubx.data.readers import DataReader
16
+ from qubx.pandaz.utils import _frame_to_str
17
+ from qubx.utils.misc import ProgressParallel, Stopwatch, get_current_user
18
+ from qubx.utils.time import handle_start_stop
19
+
20
+ from .account import SimulatedAccountProcessor
21
+ from .broker import SimulatedBroker
22
+ from .data import SimulatedDataProvider
23
+ from .utils import (
24
+ DataDecls_t,
25
+ ExchangeName_t,
26
+ SetupTypes,
27
+ SignalsProxy,
28
+ SimulatedCtrlChannel,
29
+ SimulatedLogFormatter,
30
+ SimulatedScheduler,
31
+ SimulatedTimeProvider,
32
+ SimulationDataConfig,
33
+ SimulationSetup,
34
+ StrategiesDecls_t,
35
+ SymbolOrInstrument_t,
36
+ find_instruments_and_exchanges,
37
+ recognize_simulation_configuration,
38
+ recognize_simulation_data_config,
39
+ )
40
+
41
+
42
+ def simulate(
43
+ strategies: StrategiesDecls_t,
44
+ data: DataDecls_t,
45
+ capital: float,
46
+ instruments: list[SymbolOrInstrument_t] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
47
+ commissions: str,
48
+ start: str | pd.Timestamp,
49
+ stop: str | pd.Timestamp | None = None,
50
+ exchange: ExchangeName_t | None = None,
51
+ base_currency: str = "USDT",
52
+ n_jobs: int = 1,
53
+ silent: bool = False,
54
+ aux_data: DataReader | None = None,
55
+ accurate_stop_orders_execution: bool = False,
56
+ signal_timeframe: str = "1Min",
57
+ open_close_time_indent_secs=1,
58
+ debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
59
+ show_latency_report: bool = False,
60
+ parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
61
+ ) -> list[TradingSessionResult]:
62
+ """
63
+ Backtest utility for trading strategies or signals using historical data.
64
+
65
+ Args:
66
+ - strategies (StrategiesDecls_t): Trading strategy or signals configuration.
67
+ - data (DataDecls_t): Historical data for simulation, either as a dictionary of DataFrames or a DataReader object.
68
+ - capital (float): Initial capital for the simulation.
69
+ - instruments (list[SymbolOrInstrument_t] | dict[ExchangeName_t, list[SymbolOrInstrument_t]]): List of trading instruments or a dictionary mapping exchanges to instrument lists.
70
+ - commissions (str): Commission structure for trades.
71
+ - start (str | pd.Timestamp): Start time of the simulation.
72
+ - stop (str | pd.Timestamp | None): End time of the simulation. If None, simulates until the last accessible data.
73
+ - exchange (ExchangeName_t | None): Exchange name if not specified in the instruments list.
74
+ - base_currency (str): Base currency for the simulation, default is "USDT".
75
+ - n_jobs (int): Number of parallel jobs for simulation, default is 1.
76
+ - silent (bool): If True, suppresses output during simulation.
77
+ - aux_data (DataReader | None): Auxiliary data provider (default is None).
78
+ - accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
79
+ - signal_timeframe (str): Timeframe for signals, default is "1Min".
80
+ - open_close_time_indent_secs (int): Time indent in seconds for open/close times, default is 1.
81
+ - debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None): Logging level for debugging.
82
+ - show_latency_report: If True, shows simulator's latency report.
83
+
84
+ Returns:
85
+ - list[TradingSessionResult]: A list of TradingSessionResult objects containing the results of each simulation setup.
86
+ """
87
+
88
+ # - setup logging
89
+ QubxLogConfig.set_log_level(debug.upper() if debug else "WARNING")
90
+
91
+ # - we need to reset stopwatch
92
+ Stopwatch().reset()
93
+
94
+ # - process instruments:
95
+ _instruments, _exchanges = find_instruments_and_exchanges(instruments, exchange)
96
+
97
+ # - check we have exchange
98
+ if not _exchanges:
99
+ logger.error(
100
+ _msg
101
+ := "No exchange information provided - you can specify it by exchange parameter or use <yellow>EXCHANGE:SYMBOL</yellow> format for symbols"
102
+ )
103
+ raise SimulationError(_msg)
104
+
105
+ # - check if instruments are from the same exchange (mmulti-exchanges is not supported yet)
106
+ if len(_exchanges) > 1:
107
+ logger.error(
108
+ _msg := f"Multiple exchanges found: {', '.join(_exchanges)} - this mode is not supported yet in Qubx !"
109
+ )
110
+ raise SimulationError(_msg)
111
+
112
+ exchange = _exchanges[0]
113
+
114
+ # - recognize provided data
115
+ data_setup = recognize_simulation_data_config(data, _instruments, exchange, open_close_time_indent_secs, aux_data)
116
+
117
+ # - recognize setup: it can be either a strategy or set of signals
118
+ simulation_setups = recognize_simulation_configuration(
119
+ "",
120
+ strategies,
121
+ _instruments,
122
+ exchange,
123
+ capital,
124
+ base_currency,
125
+ commissions,
126
+ signal_timeframe,
127
+ accurate_stop_orders_execution,
128
+ )
129
+ if not simulation_setups:
130
+ logger.error(
131
+ _msg
132
+ := "Can't recognize setup - it should be a strategy, a set of signals or list of signals/strategies + tracker !"
133
+ )
134
+ raise SimulationError(_msg)
135
+
136
+ # - preprocess start and stop and convert to datetime if necessary
137
+ if stop is None:
138
+ # - check stop time : here we try to backtest till now (may be we need to get max available time from data reader ?)
139
+ stop = pd.Timestamp.now(tz="UTC").astimezone(None)
140
+
141
+ _start, _stop = handle_start_stop(start, stop, convert=pd.Timestamp)
142
+ assert isinstance(_start, pd.Timestamp) and isinstance(_stop, pd.Timestamp), "Invalid start and stop times"
143
+
144
+ # - run simulations
145
+ return _run_setups(
146
+ simulation_setups,
147
+ data_setup,
148
+ _start,
149
+ _stop,
150
+ n_jobs=n_jobs,
151
+ silent=silent,
152
+ show_latency_report=show_latency_report,
153
+ parallel_backend=parallel_backend,
154
+ )
155
+
156
+
157
+ def _run_setups(
158
+ strategies_setups: list[SimulationSetup],
159
+ data_setup: SimulationDataConfig,
160
+ start: pd.Timestamp,
161
+ stop: pd.Timestamp,
162
+ n_jobs: int = -1,
163
+ silent: bool = False,
164
+ show_latency_report: bool = False,
165
+ parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
166
+ ) -> list[TradingSessionResult]:
167
+ # loggers don't work well with joblib and multiprocessing in general because they contain
168
+ # open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
169
+ # in the logger configuration and specifying backtest "multiprocessing" instead of the default "loky"
170
+ # for joblib. But it works now.
171
+ # See: https://stackoverflow.com/questions/59433146/multiprocessing-logging-how-to-use-loguru-with-joblib-parallel
172
+ _main_loop_silent = len(strategies_setups) == 1
173
+ n_jobs = 1 if _main_loop_silent else n_jobs
174
+
175
+ reports = ProgressParallel(
176
+ n_jobs=n_jobs, total=len(strategies_setups), silent=_main_loop_silent, backend=parallel_backend
177
+ )(
178
+ delayed(_run_setup)(id, f"Simulated-{id}", setup, data_setup, start, stop, silent, show_latency_report)
179
+ for id, setup in enumerate(strategies_setups)
180
+ )
181
+ return reports # type: ignore
182
+
183
+
184
+ def _run_setup(
185
+ setup_id: int,
186
+ account_id: str,
187
+ setup: SimulationSetup,
188
+ data_setup: SimulationDataConfig,
189
+ start: pd.Timestamp,
190
+ stop: pd.Timestamp,
191
+ silent: bool,
192
+ show_latency_report: bool,
193
+ ) -> TradingSessionResult:
194
+ _stop = pd.Timestamp(stop)
195
+
196
+ # - fees for this exchange
197
+ tcc = lookup.fees.find(setup.exchange.lower(), setup.commissions)
198
+ if tcc is None:
199
+ raise SimulationConfigError(
200
+ f"Can't find transaction costs calculator for '{setup.exchange}' for specification '{setup.commissions}' !"
201
+ )
202
+
203
+ channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
204
+ simulated_clock = SimulatedTimeProvider(np.datetime64(start, "ns"))
205
+
206
+ # - we want to see simulate time in log messages
207
+ QubxLogConfig.setup_logger(QubxLogConfig.get_log_level(), SimulatedLogFormatter(simulated_clock).formatter)
208
+
209
+ logger.debug(
210
+ f"[<y>simulator</y>] :: Preparing simulated trading on <g>{setup.exchange.upper()}</g> for {setup.capital} {setup.base_currency}..."
211
+ )
212
+
213
+ account = SimulatedAccountProcessor(
214
+ account_id=account_id,
215
+ channel=channel,
216
+ base_currency=setup.base_currency,
217
+ initial_capital=setup.capital,
218
+ time_provider=simulated_clock,
219
+ tcc=tcc,
220
+ accurate_stop_orders_execution=setup.accurate_stop_orders_execution,
221
+ )
222
+ scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
223
+ broker = SimulatedBroker(channel, account, setup.exchange)
224
+ data_provider = SimulatedDataProvider(
225
+ exchange_id=setup.exchange,
226
+ channel=channel,
227
+ scheduler=scheduler,
228
+ time_provider=simulated_clock,
229
+ account=account,
230
+ readers=data_setup.data_providers,
231
+ open_close_time_indent_secs=data_setup.adjusted_open_close_time_indent_secs,
232
+ )
233
+
234
+ # - it will store simulation results into memory
235
+ logs_writer = InMemoryLogsWriter(account_id, setup.name, "0")
236
+ strat: IStrategy | None = None
237
+
238
+ match setup.setup_type:
239
+ case SetupTypes.STRATEGY:
240
+ strat = setup.generator # type: ignore
241
+
242
+ case SetupTypes.STRATEGY_AND_TRACKER:
243
+ strat = setup.generator # type: ignore
244
+ strat.tracker = lambda ctx: setup.tracker # type: ignore
245
+
246
+ case SetupTypes.SIGNAL:
247
+ strat = SignalsProxy(timeframe=setup.signal_timeframe)
248
+ data_provider.set_generated_signals(setup.generator) # type: ignore
249
+
250
+ # - we don't need any unexpected triggerings
251
+ _stop = min(setup.generator.index[-1], _stop) # type: ignore
252
+
253
+ case SetupTypes.SIGNAL_AND_TRACKER:
254
+ strat = SignalsProxy(timeframe=setup.signal_timeframe)
255
+ strat.tracker = lambda ctx: setup.tracker
256
+ data_provider.set_generated_signals(setup.generator) # type: ignore
257
+
258
+ # - we don't need any unexpected triggerings
259
+ _stop = min(setup.generator.index[-1], _stop) # type: ignore
260
+
261
+ case _:
262
+ raise SimulationError(f"Unsupported setup type: {setup.setup_type} !")
263
+
264
+ if not isinstance(strat, IStrategy):
265
+ raise SimulationConfigError(f"Strategy should be an instance of IStrategy, but got {strat} !")
266
+
267
+ # - get aux data provider
268
+ _aux_data = data_setup.get_timeguarded_aux_reader(simulated_clock)
269
+
270
+ ctx = StrategyContext(
271
+ strategy=strat,
272
+ broker=broker,
273
+ data_provider=data_provider,
274
+ account=account,
275
+ scheduler=scheduler,
276
+ time_provider=simulated_clock,
277
+ instruments=setup.instruments,
278
+ logging=StrategyLogging(logs_writer),
279
+ aux_data_provider=_aux_data,
280
+ )
281
+
282
+ # - setup base subscription from spec
283
+ if ctx.get_base_subscription() == DataType.NONE:
284
+ logger.debug(
285
+ f"[<y>simulator</y>] :: Setting up default base subscription: {data_setup.default_base_subscription}"
286
+ )
287
+ ctx.set_base_subscription(data_setup.default_base_subscription)
288
+
289
+ # - set default on_event schedule if detected and strategy didn't set it's own schedule
290
+ if not ctx.get_event_schedule("time") and data_setup.default_trigger_schedule:
291
+ logger.debug(f"[<y>simulator</y>] :: Setting default schedule: {data_setup.default_trigger_schedule}")
292
+ ctx.set_event_schedule(data_setup.default_trigger_schedule)
293
+
294
+ # - get strategy parameters BEFORE simulation start
295
+ # potentially strategy may change it's parameters during simulation
296
+ _s_class, _s_params = "", None
297
+ if setup.setup_type in [SetupTypes.STRATEGY, SetupTypes.STRATEGY_AND_TRACKER]:
298
+ _s_params = extract_parameters_from_object(setup.generator)
299
+ _s_class = full_qualified_class_name(setup.generator)
300
+
301
+ # - start context at this point
302
+ ctx.start()
303
+
304
+ # - apply default warmup periods if strategy didn't set them
305
+ for s in ctx.get_subscriptions():
306
+ if not ctx.get_warmup(s) and (_d_wt := data_setup.default_warmups.get(s)):
307
+ logger.debug(
308
+ f"[<y>simulator</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
309
+ )
310
+ ctx.set_warmup({s: _d_wt})
311
+
312
+ def _is_known_type(t: str):
313
+ try:
314
+ DataType(t)
315
+ return True
316
+ except: # noqa: E722
317
+ return False
318
+
319
+ # - if any custom data providers are in the data spec
320
+ for t, r in data_setup.data_providers.items():
321
+ if not _is_known_type(t) or t in [DataType.TRADE, DataType.OHLC_TRADES, DataType.OHLC_QUOTES, DataType.QUOTE]:
322
+ logger.debug(f"[<y>simulator</y>] :: Subscribing to: {t}")
323
+ ctx.subscribe(t, ctx.instruments)
324
+
325
+ try:
326
+ data_provider.run(start, _stop, silent=silent) # type: ignore
327
+ except KeyboardInterrupt:
328
+ logger.error("Simulated trading interrupted by user !")
329
+
330
+ # - stop context at this point
331
+ ctx.stop()
332
+
333
+ # - service latency report
334
+ if show_latency_report:
335
+ _l_r = SW.latency_report()
336
+ if _l_r is not None:
337
+ logger.info(
338
+ "<BLUE> Time spent in simulation report </BLUE>\n<r>"
339
+ + _frame_to_str(
340
+ _l_r.sort_values("latency", ascending=False).reset_index(drop=True), "simulation", -1, -1, False
341
+ )
342
+ + "</r>"
343
+ )
344
+
345
+ return TradingSessionResult(
346
+ setup_id,
347
+ setup.name,
348
+ start,
349
+ stop,
350
+ setup.exchange,
351
+ setup.instruments,
352
+ setup.capital,
353
+ setup.base_currency,
354
+ setup.commissions,
355
+ logs_writer.get_portfolio(as_plain_dataframe=True),
356
+ logs_writer.get_executions(),
357
+ logs_writer.get_signals(),
358
+ strategy_class=_s_class,
359
+ parameters=_s_params,
360
+ is_simulation=True,
361
+ author=get_current_user(),
362
+ )