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
qubx/core/context.py ADDED
@@ -0,0 +1,420 @@
1
+ import traceback
2
+ from threading import Thread
3
+ from typing import Any, Callable
4
+
5
+ from qubx import logger
6
+ from qubx.core.basics import (
7
+ SW,
8
+ AssetBalance,
9
+ CtrlChannel,
10
+ DataType,
11
+ Instrument,
12
+ ITimeProvider,
13
+ MarketType,
14
+ Order,
15
+ OrderRequest,
16
+ Position,
17
+ dt_64,
18
+ )
19
+ from qubx.core.helpers import (
20
+ BasicScheduler,
21
+ CachedMarketDataHolder,
22
+ set_parameters_to_object,
23
+ )
24
+ from qubx.core.interfaces import (
25
+ IAccountProcessor,
26
+ IBroker,
27
+ IDataProvider,
28
+ IMarketManager,
29
+ IPositionGathering,
30
+ IProcessingManager,
31
+ IStrategy,
32
+ IStrategyContext,
33
+ ISubscriptionManager,
34
+ ITradingManager,
35
+ IUniverseManager,
36
+ PositionsTracker,
37
+ RemovalPolicy,
38
+ )
39
+ from qubx.core.loggers import StrategyLogging
40
+ from qubx.data.readers import DataReader
41
+ from qubx.gathering.simplest import SimplePositionGatherer
42
+ from qubx.trackers.sizers import FixedSizer
43
+
44
+ from .mixins import (
45
+ MarketManager,
46
+ ProcessingManager,
47
+ SubscriptionManager,
48
+ TradingManager,
49
+ UniverseManager,
50
+ )
51
+
52
+
53
+ class StrategyContext(IStrategyContext):
54
+ DEFAULT_POSITION_TRACKER: Callable[[], PositionsTracker] = lambda: PositionsTracker(
55
+ FixedSizer(1.0, amount_in_quote=False)
56
+ )
57
+
58
+ _market_data_provider: IMarketManager
59
+ _universe_manager: IUniverseManager
60
+ _subscription_manager: ISubscriptionManager
61
+ _trading_manager: ITradingManager
62
+ _processing_manager: IProcessingManager
63
+
64
+ _broker: IBroker # service for exchange API: orders managemewnt
65
+ _logging: StrategyLogging # recording all activities for the strat: execs, positions, portfolio
66
+ _data_provider: IDataProvider # market data provider
67
+ _cache: CachedMarketDataHolder
68
+ _scheduler: BasicScheduler
69
+ _initial_instruments: list[Instrument]
70
+
71
+ _thread_data_loop: Thread | None = None # market data loop
72
+ _is_initialized: bool = False
73
+
74
+ def __init__(
75
+ self,
76
+ strategy: IStrategy,
77
+ broker: IBroker,
78
+ data_provider: IDataProvider,
79
+ account: IAccountProcessor,
80
+ scheduler: BasicScheduler,
81
+ time_provider: ITimeProvider,
82
+ instruments: list[Instrument],
83
+ logging: StrategyLogging,
84
+ config: dict[str, Any] | None = None,
85
+ position_gathering: IPositionGathering | None = None, # TODO: make position gathering part of the strategy
86
+ aux_data_provider: DataReader | None = None,
87
+ ) -> None:
88
+ self.account = account
89
+ self.strategy = self.__instantiate_strategy(strategy, config)
90
+
91
+ self._time_provider = time_provider
92
+ self._broker = broker
93
+ self._data_provider = data_provider
94
+ self._logging = logging
95
+ self._scheduler = scheduler
96
+ self._initial_instruments = instruments
97
+
98
+ self._cache = CachedMarketDataHolder()
99
+
100
+ __position_tracker = self.strategy.tracker(self)
101
+ if __position_tracker is None:
102
+ __position_tracker = StrategyContext.DEFAULT_POSITION_TRACKER()
103
+
104
+ __position_gathering = position_gathering if position_gathering is not None else SimplePositionGatherer()
105
+
106
+ self._subscription_manager = SubscriptionManager(
107
+ data_provider=self._data_provider,
108
+ default_base_subscription=DataType.ORDERBOOK if not self._data_provider.is_simulation else DataType.NONE,
109
+ )
110
+ self.account.set_subscription_manager(self._subscription_manager)
111
+
112
+ self._market_data_provider = MarketManager(
113
+ time_provider=self._time_provider,
114
+ cache=self._cache,
115
+ data_provider=self._data_provider,
116
+ universe_manager=self,
117
+ aux_data_provider=aux_data_provider,
118
+ )
119
+ self._universe_manager = UniverseManager(
120
+ context=self,
121
+ strategy=self.strategy,
122
+ broker=self._data_provider,
123
+ trading_service=self._broker,
124
+ cache=self._cache,
125
+ logging=self._logging,
126
+ subscription_manager=self,
127
+ trading_manager=self,
128
+ time_provider=self,
129
+ account=self.account,
130
+ position_gathering=__position_gathering,
131
+ )
132
+ self._trading_manager = TradingManager(
133
+ time_provider=self,
134
+ broker=self._broker,
135
+ account=self.account,
136
+ strategy_name=self.strategy.__class__.__name__,
137
+ )
138
+ self._processing_manager = ProcessingManager(
139
+ context=self,
140
+ strategy=self.strategy,
141
+ logging=self._logging,
142
+ market_data=self,
143
+ subscription_manager=self,
144
+ time_provider=self,
145
+ account=self.account,
146
+ position_tracker=__position_tracker,
147
+ position_gathering=__position_gathering,
148
+ universe_manager=self._universe_manager,
149
+ cache=self._cache,
150
+ scheduler=self._scheduler,
151
+ is_simulation=self._data_provider.is_simulation,
152
+ )
153
+ self.__post_init__()
154
+
155
+ def __post_init__(self) -> None:
156
+ self.strategy.on_init(self)
157
+ # - update cache default timeframe
158
+ sub_type = self.get_base_subscription()
159
+ _, params = DataType.from_str(sub_type)
160
+ __default_timeframe = params.get("timeframe", "1sec")
161
+ self._cache.update_default_timeframe(__default_timeframe)
162
+
163
+ def start(self, blocking: bool = False):
164
+ if self._is_initialized:
165
+ raise ValueError("Strategy is already started !")
166
+
167
+ # - run cron scheduler
168
+ self._scheduler.run()
169
+
170
+ # - create incoming market data processing
171
+ databus = self._data_provider.channel
172
+ databus.register(self)
173
+
174
+ # - start account processing
175
+ self.account.start()
176
+
177
+ # - update universe with initial instruments after the strategy is initialized
178
+ self.set_universe(self._initial_instruments, skip_callback=True)
179
+
180
+ # - initialize strategy (should we do that after any first market data received ?)
181
+ if not self._is_initialized:
182
+ try:
183
+ self.strategy.on_start(self)
184
+ self._is_initialized = True
185
+ except Exception as strat_error:
186
+ logger.error(
187
+ f"[StrategyContext] :: Strategy {self.strategy.__class__.__name__} raised an exception in on_start: {strat_error}"
188
+ )
189
+ logger.error(traceback.format_exc())
190
+ return
191
+
192
+ # - for live we run loop
193
+ if not self._data_provider.is_simulation:
194
+ self._thread_data_loop = Thread(target=self.__process_incoming_data_loop, args=(databus,), daemon=True)
195
+ self._thread_data_loop.start()
196
+ logger.info("[StrategyContext] :: strategy is started in thread")
197
+ if blocking:
198
+ self._thread_data_loop.join()
199
+
200
+ def stop(self):
201
+ # - invoke strategy's stop code
202
+ try:
203
+ self.strategy.on_stop(self)
204
+ except Exception as strat_error:
205
+ logger.error(
206
+ f"[<y>StrategyContext</y>] :: Strategy {self.strategy.__class__.__name__} raised an exception in on_stop: {strat_error}"
207
+ )
208
+ logger.opt(colors=False).error(traceback.format_exc())
209
+
210
+ if self._thread_data_loop:
211
+ self._data_provider.close()
212
+ self._data_provider.channel.stop()
213
+ self._thread_data_loop.join()
214
+ self._thread_data_loop = None
215
+
216
+ # - stop account processing
217
+ self.account.stop()
218
+
219
+ # - close logging
220
+ self._logging.close()
221
+
222
+ def is_running(self):
223
+ return self._thread_data_loop is not None and self._thread_data_loop.is_alive()
224
+
225
+ @property
226
+ def is_simulation(self) -> bool:
227
+ return self._data_provider.is_simulation
228
+
229
+ # IAccountViewer delegation
230
+
231
+ # capital information
232
+ def get_capital(self) -> float:
233
+ return self.account.get_capital()
234
+
235
+ def get_total_capital(self) -> float:
236
+ return self.account.get_total_capital()
237
+
238
+ # balance and position information
239
+ def get_balances(self) -> dict[str, AssetBalance]:
240
+ return dict(self.account.get_balances())
241
+
242
+ def get_positions(self) -> dict[Instrument, Position]:
243
+ return dict(self.account.get_positions())
244
+
245
+ def get_position(self, instrument: Instrument) -> Position:
246
+ return self.account.get_position(instrument)
247
+
248
+ @property
249
+ def positions(self):
250
+ return self.account.positions
251
+
252
+ def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
253
+ return self.account.get_orders(instrument)
254
+
255
+ def position_report(self) -> dict:
256
+ return self.account.position_report()
257
+
258
+ # leverage information
259
+ def get_leverage(self, instrument: Instrument) -> float:
260
+ return self.account.get_leverage(instrument)
261
+
262
+ def get_leverages(self) -> dict[Instrument, float]:
263
+ return self.account.get_leverages()
264
+
265
+ def get_net_leverage(self) -> float:
266
+ return self.account.get_net_leverage()
267
+
268
+ def get_gross_leverage(self) -> float:
269
+ return self.account.get_gross_leverage()
270
+
271
+ # margin information
272
+ def get_total_required_margin(self) -> float:
273
+ return self.account.get_total_required_margin()
274
+
275
+ def get_available_margin(self) -> float:
276
+ return self.account.get_available_margin()
277
+
278
+ def get_margin_ratio(self) -> float:
279
+ return self.account.get_margin_ratio()
280
+
281
+ # IMarketDataProvider delegation
282
+ def time(self) -> dt_64:
283
+ return self._market_data_provider.time()
284
+
285
+ def ohlc(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None):
286
+ return self._market_data_provider.ohlc(instrument, timeframe, length)
287
+
288
+ def quote(self, instrument: Instrument):
289
+ return self._market_data_provider.quote(instrument)
290
+
291
+ def get_data(self, instrument: Instrument, sub_type: str) -> list[Any]:
292
+ return self._market_data_provider.get_data(instrument, sub_type)
293
+
294
+ def get_aux_data(self, data_id: str, **parameters):
295
+ return self._market_data_provider.get_aux_data(data_id, **parameters)
296
+
297
+ def get_instruments(self):
298
+ return self._market_data_provider.get_instruments()
299
+
300
+ def query_instrument(self, symbol: str, exchange: str | None = None) -> Instrument | None:
301
+ return self._market_data_provider.query_instrument(
302
+ symbol, exchange if exchange is not None else self.exchanges[0]
303
+ )
304
+
305
+ # ITradingManager delegation
306
+ def trade(self, instrument: Instrument, amount: float, price: float | None = None, time_in_force="gtc", **options):
307
+ return self._trading_manager.trade(instrument, amount, price, time_in_force, **options)
308
+
309
+ def submit_orders(self, order_requests: list[OrderRequest]) -> list[Order]:
310
+ return self._trading_manager.submit_orders(order_requests)
311
+
312
+ def set_target_position(
313
+ self, instrument: Instrument, target: float, price: float | None = None, **options
314
+ ) -> Order:
315
+ return self._trading_manager.set_target_position(instrument, target, price, **options)
316
+
317
+ def close_position(self, instrument: Instrument) -> None:
318
+ return self._trading_manager.close_position(instrument)
319
+
320
+ def close_positions(self, market_type: MarketType | None = None) -> None:
321
+ return self._trading_manager.close_positions(market_type)
322
+
323
+ def cancel_order(self, order_id: str) -> None:
324
+ return self._trading_manager.cancel_order(order_id)
325
+
326
+ def cancel_orders(self, instrument: Instrument):
327
+ return self._trading_manager.cancel_orders(instrument)
328
+
329
+ # IUniverseManager delegation
330
+ def set_universe(
331
+ self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
332
+ ):
333
+ return self._universe_manager.set_universe(instruments, skip_callback, if_has_position_then)
334
+
335
+ def add_instruments(self, instruments: list[Instrument]):
336
+ return self._universe_manager.add_instruments(instruments)
337
+
338
+ def remove_instruments(self, instruments: list[Instrument]):
339
+ return self._universe_manager.remove_instruments(instruments)
340
+
341
+ @property
342
+ def instruments(self):
343
+ return self._universe_manager.instruments
344
+
345
+ @property
346
+ def exchanges(self) -> list[str]:
347
+ return self._trading_manager.exchanges()
348
+
349
+ # ISubscriptionManager delegation
350
+ def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None):
351
+ return self._subscription_manager.subscribe(subscription_type, instruments)
352
+
353
+ def unsubscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None):
354
+ return self._subscription_manager.unsubscribe(subscription_type, instruments)
355
+
356
+ def has_subscription(self, instrument: Instrument, subscription_type: str):
357
+ return self._subscription_manager.has_subscription(instrument, subscription_type)
358
+
359
+ def get_subscriptions(self, instrument: Instrument | None = None) -> list[str]:
360
+ return self._subscription_manager.get_subscriptions(instrument)
361
+
362
+ def get_base_subscription(self) -> str:
363
+ return self._subscription_manager.get_base_subscription()
364
+
365
+ def set_base_subscription(self, subscription_type: str):
366
+ return self._subscription_manager.set_base_subscription(subscription_type)
367
+
368
+ def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
369
+ return self._subscription_manager.get_subscribed_instruments(subscription_type)
370
+
371
+ def get_warmup(self, subscription_type: str) -> str | None:
372
+ return self._subscription_manager.get_warmup(subscription_type)
373
+
374
+ def set_warmup(self, configs: dict[Any, str]):
375
+ return self._subscription_manager.set_warmup(configs)
376
+
377
+ def commit(self):
378
+ return self._subscription_manager.commit()
379
+
380
+ @property
381
+ def auto_subscribe(self) -> bool:
382
+ return self._subscription_manager.auto_subscribe
383
+
384
+ @auto_subscribe.setter
385
+ def auto_subscribe(self, value: bool):
386
+ self._subscription_manager.auto_subscribe = value
387
+
388
+ # IProcessingManager delegation
389
+ def process_data(self, instrument: Instrument, d_type: str, data: Any, is_historical: bool):
390
+ return self._processing_manager.process_data(instrument, d_type, data, is_historical)
391
+
392
+ def set_fit_schedule(self, schedule: str):
393
+ return self._processing_manager.set_fit_schedule(schedule)
394
+
395
+ def set_event_schedule(self, schedule: str):
396
+ return self._processing_manager.set_event_schedule(schedule)
397
+
398
+ def get_event_schedule(self, event_id: str) -> str | None:
399
+ return self._processing_manager.get_event_schedule(event_id)
400
+
401
+ def is_fitted(self) -> bool:
402
+ return self._processing_manager.is_fitted()
403
+
404
+ # private methods
405
+ def __process_incoming_data_loop(self, channel: CtrlChannel):
406
+ logger.info("[StrategyContext] :: Start processing market data")
407
+ while channel.control.is_set():
408
+ with SW("StrategyContext._process_incoming_data"):
409
+ # - waiting for incoming market data
410
+ instrument, d_type, data, hist = channel.receive()
411
+ if self.process_data(instrument, d_type, data, hist):
412
+ channel.stop()
413
+ break
414
+ logger.info("[StrategyContext] :: Market data processing stopped")
415
+
416
+ def __instantiate_strategy(self, strategy: IStrategy, config: dict[str, Any] | None) -> IStrategy:
417
+ __strategy = strategy() if isinstance(strategy, type) else strategy
418
+ __strategy.ctx = self
419
+ set_parameters_to_object(__strategy, **config if config else {})
420
+ return __strategy
@@ -0,0 +1,38 @@
1
+ class BaseError(Exception):
2
+ pass
3
+
4
+
5
+ class ExchangeError(BaseError):
6
+ pass
7
+
8
+
9
+ class BadRequest(ExchangeError):
10
+ pass
11
+
12
+
13
+ class InvalidOrder(ExchangeError):
14
+ pass
15
+
16
+
17
+ class OrderNotFound(InvalidOrder):
18
+ pass
19
+
20
+
21
+ class NotSupported(ExchangeError):
22
+ pass
23
+
24
+
25
+ class QueueTimeout(BaseError):
26
+ pass
27
+
28
+
29
+ class StrategyExceededMaxNumberOfRuntimeFailuresError(Exception):
30
+ pass
31
+
32
+
33
+ class SimulationError(Exception):
34
+ pass
35
+
36
+
37
+ class SimulationConfigError(Exception):
38
+ pass