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,1150 @@
1
+ """
2
+ This module defines interfaces and classes related to trading strategies.
3
+
4
+ This module includes:
5
+ - Trading service providers
6
+ - Broker service providers
7
+ - Market data providers
8
+ - Strategy contexts
9
+ - Position tracking and management
10
+ """
11
+
12
+ import traceback
13
+ from typing import Any, Dict, List, Literal, Set, Tuple
14
+
15
+ import numpy as np
16
+ import pandas as pd
17
+
18
+ from qubx import logger
19
+ from qubx.core.basics import (
20
+ AssetBalance,
21
+ CtrlChannel,
22
+ Deal,
23
+ Instrument,
24
+ ITimeProvider,
25
+ MarketEvent,
26
+ MarketType,
27
+ Order,
28
+ OrderRequest,
29
+ Position,
30
+ Signal,
31
+ TargetPosition,
32
+ Timestamped,
33
+ TriggerEvent,
34
+ dt_64,
35
+ )
36
+ from qubx.core.helpers import set_parameters_to_object
37
+ from qubx.core.series import OHLCV, Bar, Quote
38
+
39
+ RemovalPolicy = Literal["close", "wait_for_close", "wait_for_change"]
40
+
41
+
42
+ class IAccountViewer:
43
+ account_id: str
44
+
45
+ def get_base_currency(self) -> str:
46
+ """Get the base currency for the account.
47
+
48
+ Returns:
49
+ str: The base currency.
50
+ """
51
+ ...
52
+
53
+ ########################################################
54
+ # Capital information
55
+ ########################################################
56
+ def get_capital(self) -> float:
57
+ """Get the available free capital in the account.
58
+
59
+ Returns:
60
+ float: The amount of free capital available for trading
61
+ """
62
+ ...
63
+
64
+ def get_total_capital(self) -> float:
65
+ """Get the total capital in the account including positions value.
66
+
67
+ Returns:
68
+ float: Total account capital
69
+ """
70
+ ...
71
+
72
+ ########################################################
73
+ # Balance and position information
74
+ ########################################################
75
+ def get_balances(self) -> dict[str, AssetBalance]:
76
+ """Get all currency balances.
77
+
78
+ Returns:
79
+ dict[str, AssetBalance]: Dictionary mapping currency codes to AssetBalance objects
80
+ """
81
+ ...
82
+
83
+ def get_positions(self) -> dict[Instrument, Position]:
84
+ """Get all current positions.
85
+
86
+ Returns:
87
+ dict[Instrument, Position]: Dictionary mapping instruments to their positions
88
+ """
89
+ ...
90
+
91
+ def get_position(self, instrument: Instrument) -> Position:
92
+ """Get the current position for a specific instrument.
93
+
94
+ Args:
95
+ instrument: The instrument to get the position for
96
+
97
+ Returns:
98
+ Position: The position object
99
+ """
100
+ ...
101
+
102
+ @property
103
+ def positions(self) -> dict[Instrument, Position]:
104
+ """[Deprecated: Use get_positions()] Get all current positions.
105
+
106
+ Returns:
107
+ dict[Instrument, Position]: Dictionary mapping instruments to their positions
108
+ """
109
+ return self.get_positions()
110
+
111
+ def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
112
+ """Get active orders, optionally filtered by instrument.
113
+
114
+ Args:
115
+ instrument: Optional instrument to filter orders by
116
+
117
+ Returns:
118
+ dict[str, Order]: Dictionary mapping order IDs to Order objects
119
+ """
120
+ ...
121
+
122
+ def position_report(self) -> dict:
123
+ """Get detailed report of all positions.
124
+
125
+ Returns:
126
+ dict: Dictionary containing position details including quantities, prices, PnL etc.
127
+ """
128
+ ...
129
+
130
+ ########################################################
131
+ # Leverage information
132
+ ########################################################
133
+ def get_leverage(self, instrument: Instrument) -> float:
134
+ """Get the leverage used for a specific instrument.
135
+
136
+ Args:
137
+ instrument: The instrument to check
138
+
139
+ Returns:
140
+ float: Current leverage ratio for the instrument
141
+ """
142
+ ...
143
+
144
+ def get_leverages(self) -> dict[Instrument, float]:
145
+ """Get leverages for all instruments.
146
+
147
+ Returns:
148
+ dict[Instrument, float]: Dictionary mapping instruments to their leverage ratios
149
+ """
150
+ ...
151
+
152
+ def get_net_leverage(self) -> float:
153
+ """Get the net leverage across all positions.
154
+
155
+ Returns:
156
+ float: Net leverage ratio
157
+ """
158
+ ...
159
+
160
+ def get_gross_leverage(self) -> float:
161
+ """Get the gross leverage across all positions.
162
+
163
+ Returns:
164
+ float: Gross leverage ratio
165
+ """
166
+ ...
167
+
168
+ ########################################################
169
+ # Margin information
170
+ # Used for margin, swap, futures, options trading
171
+ ########################################################
172
+ def get_total_required_margin(self) -> float:
173
+ """Get total margin required for all positions.
174
+
175
+ Returns:
176
+ float: Total required margin
177
+ """
178
+ ...
179
+
180
+ def get_available_margin(self) -> float:
181
+ """Get available margin for new positions.
182
+
183
+ Returns:
184
+ float: Available margin
185
+ """
186
+ ...
187
+
188
+ def get_margin_ratio(self) -> float:
189
+ """Get current margin ratio.
190
+
191
+ Formula: (total capital + positions value) / total required margin
192
+
193
+ Example:
194
+ If total capital is 1000, positions value is 2000, and total required margin is 3000,
195
+ the margin ratio would be (1000 + 2000) / 3000 = 1.0
196
+
197
+ Returns:
198
+ float: Current margin ratio
199
+ """
200
+ ...
201
+
202
+ def get_reserved(self, instrument: Instrument) -> float:
203
+ """[Deprecated] Get reserved margin for a specific instrument.
204
+
205
+ Args:
206
+ instrument: The instrument to check
207
+
208
+ Returns:
209
+ float: Reserved margin for the instrument
210
+ """
211
+ return 0.0
212
+
213
+
214
+ class IBroker:
215
+ """Broker provider interface for managing trading operations.
216
+
217
+ Handles account operations, order placement, and position tracking.
218
+ """
219
+
220
+ channel: CtrlChannel
221
+
222
+ @property
223
+ def is_simulated_trading(self) -> bool:
224
+ """
225
+ Check if the broker is in simulation mode.
226
+ """
227
+ ...
228
+
229
+ # TODO: think about replacing with async methods
230
+ def send_order(
231
+ self,
232
+ instrument: Instrument,
233
+ order_side: str,
234
+ order_type: str,
235
+ amount: float,
236
+ price: float | None = None,
237
+ client_id: str | None = None,
238
+ time_in_force: str = "gtc",
239
+ **optional,
240
+ ) -> Order:
241
+ """Sends an order to the trading service.
242
+
243
+ Args:
244
+ instrument: The instrument to trade.
245
+ order_side: Order side ("buy" or "sell").
246
+ order_type: Type of order ("market" or "limit").
247
+ amount: Amount of instrument to trade.
248
+ price: Price for limit orders.
249
+ client_id: Client-specified order ID.
250
+ time_in_force: Time in force for order (default: "gtc").
251
+ **optional: Additional order parameters.
252
+
253
+ Returns:
254
+ Order: The created order object.
255
+ """
256
+ raise NotImplementedError("send_order is not implemented")
257
+
258
+ def cancel_order(self, order_id: str) -> Order | None:
259
+ """Cancel an existing order.
260
+
261
+ Args:
262
+ order_id: The ID of the order to cancel.
263
+
264
+ Returns:
265
+ Order | None: The cancelled Order object if successful, None otherwise.
266
+ """
267
+ raise NotImplementedError("cancel_order is not implemented")
268
+
269
+ def cancel_orders(self, instrument: Instrument) -> None:
270
+ """Cancel all orders for an instrument.
271
+
272
+ Args:
273
+ instrument: The instrument to cancel orders for.
274
+ """
275
+ raise NotImplementedError("cancel_orders is not implemented")
276
+
277
+ def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
278
+ """Update an existing order.
279
+
280
+ Args:
281
+ order_id: The ID of the order to update.
282
+ price: New price for the order.
283
+ amount: New amount for the order.
284
+
285
+ Returns:
286
+ Order: The updated Order object if successful
287
+
288
+ Raises:
289
+ NotImplementedError: If the method is not implemented
290
+ OrderNotFound: If the order is not found
291
+ BadRequest: If the request is invalid
292
+ """
293
+ raise NotImplementedError("update_order is not implemented")
294
+
295
+ def exchange(self) -> str:
296
+ """
297
+ Return the name of the exchange this broker is connected to.
298
+ """
299
+ raise NotImplementedError("exchange() is not implemented")
300
+
301
+
302
+ class IDataProvider:
303
+ time_provider: ITimeProvider
304
+ channel: CtrlChannel
305
+
306
+ def subscribe(
307
+ self,
308
+ subscription_type: str,
309
+ instruments: Set[Instrument],
310
+ reset: bool = False,
311
+ ) -> None:
312
+ """
313
+ Subscribe to market data for a list of instruments.
314
+
315
+ Args:
316
+ subscription_type: Type of subscription
317
+ instruments: Set of instruments to subscribe to
318
+ reset: Reset existing instruments for the subscription type. Default is False.
319
+ """
320
+ ...
321
+
322
+ def unsubscribe(self, subscription_type: str | None, instruments: Set[Instrument]) -> None:
323
+ """
324
+ Unsubscribe from market data for a list of instruments.
325
+
326
+ Args:
327
+ subscription_type: Type of subscription to unsubscribe from (optional)
328
+ instruments: Set of instruments to unsubscribe from
329
+ """
330
+ ...
331
+
332
+ def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
333
+ """
334
+ Check if an instrument has a subscription.
335
+
336
+ Args:
337
+ instrument: Instrument to check
338
+ subscription_type: Type of subscription to check
339
+
340
+ Returns:
341
+ bool: True if instrument has the subscription
342
+ """
343
+ ...
344
+
345
+ def get_subscriptions(self, instrument: Instrument | None = None) -> List[str]:
346
+ """
347
+ Get all subscriptions for an instrument.
348
+
349
+ Args:
350
+ instrument (optional): Instrument to get subscriptions for. If None, all subscriptions are returned.
351
+
352
+ Returns:
353
+ List[str]: List of subscriptions
354
+ """
355
+ ...
356
+
357
+ def get_subscribed_instruments(self, subscription_type: str | None = None) -> List[Instrument]:
358
+ """
359
+ Get a list of instruments that are subscribed to a specific subscription type.
360
+
361
+ Args:
362
+ subscription_type: Type of subscription to filter by (optional)
363
+
364
+ Returns:
365
+ List[Instrument]: List of subscribed instruments
366
+ """
367
+ ...
368
+
369
+ def warmup(self, configs: Dict[Tuple[str, Instrument], str]) -> None:
370
+ """
371
+ Run warmup for subscriptions.
372
+
373
+ Args:
374
+ configs: Dictionary of (subscription type, instrument) pairs and warmup periods.
375
+
376
+ Example:
377
+ warmup({
378
+ (DataType.OHLC["1h"], instr1): "30d",
379
+ (DataType.OHLC["1Min"], instr1): "6h",
380
+ (DataType.OHLC["1Sec"], instr2): "5Min",
381
+ (DataType.TRADE, instr2): "1h",
382
+ })
383
+ """
384
+ ...
385
+
386
+ def get_ohlc(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
387
+ """
388
+ Get historical OHLC data for an instrument.
389
+ """
390
+ ...
391
+
392
+ def get_quote(self, instrument: Instrument) -> Quote:
393
+ """
394
+ Get the latest quote for an instrument.
395
+ """
396
+ ...
397
+
398
+ @property
399
+ def is_simulation(self) -> bool:
400
+ """
401
+ Check if data provider is in simulation mode.
402
+ """
403
+ ...
404
+
405
+ def close(self):
406
+ """
407
+ Close the data provider.
408
+ """
409
+ ...
410
+
411
+ def exchange(self) -> str:
412
+ """
413
+ Return the name of the exchange this provider reads data
414
+ """
415
+ raise NotImplementedError("exchange() is not implemented")
416
+
417
+
418
+ class IMarketManager(ITimeProvider):
419
+ """Interface for market data providing class"""
420
+
421
+ def ohlc(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None) -> OHLCV:
422
+ """Get OHLCV data for an instrument. If length is larger then available cached data, it will be requested from the broker.
423
+
424
+ Args:
425
+ instrument: The instrument to get data for
426
+ timeframe (optional): The timeframe of the data. If None, the default timeframe is used.
427
+ length (optional): Number of bars to retrieve. If None, full cached data is returned.
428
+
429
+ Returns:
430
+ OHLCV: The OHLCV data series
431
+ """
432
+ ...
433
+
434
+ def quote(self, instrument: Instrument) -> Quote | None:
435
+ """Get latest quote for an instrument.
436
+
437
+ Args:
438
+ instrument: The instrument to get quote for
439
+
440
+ Returns:
441
+ Quote | None: The latest quote or None if not available
442
+ """
443
+ ...
444
+
445
+ def get_data(self, instrument: Instrument, sub_type: str) -> list[Any]:
446
+ """Get data for an instrument. This method is used for getting data for custom subscription types.
447
+ Could be used for orderbook, trades, liquidations, funding rates, etc.
448
+
449
+ Args:
450
+ instrument: The instrument to get data for
451
+ sub_type: The subscription type of data to get
452
+
453
+ Returns:
454
+ List[Any]: The data
455
+ """
456
+ ...
457
+
458
+ def get_aux_data(self, data_id: str, **parametes) -> pd.DataFrame | None:
459
+ """Get auxiliary data by ID.
460
+
461
+ Args:
462
+ data_id: Identifier for the auxiliary data
463
+ **parametes: Additional parameters for the data request
464
+
465
+ Returns:
466
+ pd.DataFrame | None: The auxiliary data or None if not found
467
+ """
468
+ ...
469
+
470
+ def get_instruments(self) -> list[Instrument]:
471
+ """Get list of subscribed instruments.
472
+
473
+ Returns:
474
+ list[Instrument]: List of subscribed instruments
475
+ """
476
+ ...
477
+
478
+ def query_instrument(self, symbol: str, exchange: str | None = None) -> Instrument | None:
479
+ """Query instrument in lookup by symbol and exchange.
480
+
481
+ Args:
482
+ symbol: The symbol to look up
483
+ exchange: The exchange to look up or None (current exchange is used)
484
+
485
+ Returns:
486
+ Instrument | None: The instrument if found, None otherwise
487
+ """
488
+ ...
489
+
490
+ def exchanges(self) -> list[str]: ...
491
+
492
+
493
+ class ITradingManager:
494
+ """Manages order operations."""
495
+
496
+ def trade(
497
+ self,
498
+ instrument: Instrument,
499
+ amount: float,
500
+ price: float | None = None,
501
+ time_in_force="gtc",
502
+ **options,
503
+ ) -> Order:
504
+ """Place a trade order.
505
+
506
+ Args:
507
+ instrument: The instrument to trade
508
+ amount: Amount to trade (positive for buy, negative for sell)
509
+ price: Optional limit price
510
+ time_in_force: Time in force for the order
511
+ **options: Additional order options
512
+
513
+ Returns:
514
+ Order: The created order
515
+ """
516
+ ...
517
+
518
+ def submit_orders(self, order_requests: list[OrderRequest]) -> list[Order]:
519
+ """Submit multiple orders to the exchange."""
520
+ ...
521
+
522
+ def set_target_position(
523
+ self, instrument: Instrument, target: float, price: float | None = None, **options
524
+ ) -> Order:
525
+ """Set target position for an instrument.
526
+
527
+ Args:
528
+ instrument: The instrument to set target position for
529
+ target: Target position size
530
+ price: Optional limit price
531
+ time_in_force: Time in force for the order
532
+ **options: Additional order options
533
+
534
+ Returns:
535
+ Order: The created order
536
+ """
537
+ ...
538
+
539
+ def close_position(self, instrument: Instrument) -> None:
540
+ """Close position for an instrument.
541
+
542
+ Args:
543
+ instrument: The instrument to close position for
544
+ """
545
+ ...
546
+
547
+ def close_positions(self, market_type: MarketType | None = None) -> None:
548
+ """Close all positions."""
549
+ ...
550
+
551
+ def cancel_order(self, order_id: str) -> None:
552
+ """Cancel a specific order.
553
+
554
+ Args:
555
+ order_id: ID of the order to cancel
556
+ """
557
+ ...
558
+
559
+ def cancel_orders(self, instrument: Instrument) -> None:
560
+ """Cancel all orders for an instrument.
561
+
562
+ Args:
563
+ instrument: The instrument to cancel orders for
564
+ """
565
+ ...
566
+
567
+ def exchanges(self) -> list[str]: ...
568
+
569
+
570
+ class IUniverseManager:
571
+ """Manages universe updates."""
572
+
573
+ def set_universe(
574
+ self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
575
+ ):
576
+ """Set the trading universe.
577
+
578
+ Args:
579
+ instruments: List of instruments in the universe
580
+ skip_callback: Skip callback to the strategy
581
+ if_has_position_then: What to do if the instrument has a position
582
+ - “close” (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
583
+ - “wait_for_close” - keep instrument and it’s position until it’s closed from strategy (or risk management), then remove instrument from strategy
584
+ - “wait_for_change” - keep instrument and position until strategy would try to change it - then close position and remove instrument
585
+ """
586
+ ...
587
+
588
+ def add_instruments(self, instruments: list[Instrument]):
589
+ """Add instruments to the trading universe.
590
+
591
+ Args:
592
+ instruments: List of instruments to add
593
+ """
594
+ ...
595
+
596
+ def remove_instruments(self, instruments: list[Instrument], if_has_position_then: RemovalPolicy = "close"):
597
+ """Remove instruments from the trading universe.
598
+
599
+ Args:
600
+ instruments: List of instruments to remove
601
+ if_has_position_then: What to do if the instrument has a position
602
+ - “close” (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
603
+ - “wait_for_close” - keep instrument and it’s position until it’s closed from strategy (or risk management), then remove instrument from strategy
604
+ - “wait_for_change” - keep instrument and position until strategy would try to change it - then close position and remove instrument
605
+ """
606
+ ...
607
+
608
+ @property
609
+ def instruments(self) -> list[Instrument]:
610
+ """
611
+ Get the list of instruments in the universe.
612
+ """
613
+ ...
614
+
615
+ def on_alter_position(self, instrument: Instrument) -> None:
616
+ """
617
+ Called when the position of an instrument changes.
618
+ It can be used for postponed unsubscribed events
619
+ """
620
+ ...
621
+
622
+ def is_trading_allowed(self, instrument: Instrument) -> bool:
623
+ """
624
+ Check if trading is allowed for an instrument because of the instrument's trading policy.
625
+ """
626
+ ...
627
+
628
+
629
+ class ISubscriptionManager:
630
+ """Manages subscriptions."""
631
+
632
+ def subscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None) -> None:
633
+ """Subscribe to market data for an instrument.
634
+
635
+ Args:
636
+ subscription_type: Type of subscription. If None, the base subscription type is used.
637
+ instruments: A list of instrument of instrument to subscribe to
638
+ """
639
+ ...
640
+
641
+ def unsubscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None) -> None:
642
+ """Unsubscribe from market data for an instrument.
643
+
644
+ Args:
645
+ subscription_type: Type of subscription to unsubscribe from (e.g. DataType.OHLC)
646
+ instruments (optional): A list of instruments or instrument to unsubscribe from.
647
+ """
648
+ ...
649
+
650
+ def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
651
+ """Check if subscription exists.
652
+
653
+ Args:
654
+ subscription_type: Type of subscription
655
+ instrument: Instrument to check
656
+
657
+ Returns:
658
+ bool: True if subscription exists
659
+ """
660
+ ...
661
+
662
+ def get_base_subscription(self) -> str:
663
+ """
664
+ Get the main subscription which should be used for the simulation.
665
+ This data is used for updating the internal OHLCV data series.
666
+ By default, simulation uses 1h OHLCV bars and live trading uses orderbook data.
667
+ """
668
+ ...
669
+
670
+ def set_base_subscription(self, subscription_type: str) -> None:
671
+ """
672
+ Set the main subscription which should be used for the simulation.
673
+
674
+ Args:
675
+ subscription_type: Type of subscription (e.g. DataType.OHLC, DataType.OHLC["1h"])
676
+ """
677
+ ...
678
+
679
+ def get_subscriptions(self, instrument: Instrument | None = None) -> List[str]:
680
+ """
681
+ Get all subscriptions for an instrument.
682
+
683
+ Args:
684
+ instrument: Instrument to get subscriptions for (optional)
685
+
686
+ Returns:
687
+ List[str]: List of subscriptions
688
+ """
689
+ ...
690
+
691
+ def get_subscribed_instruments(self, subscription_type: str | None = None) -> List[Instrument]:
692
+ """
693
+ Get a list of instruments that are subscribed to a specific subscription type.
694
+
695
+ Args:
696
+ subscription_type: Type of subscription to filter by (optional)
697
+
698
+ Returns:
699
+ List[Instrument]: List of subscribed instruments
700
+ """
701
+ ...
702
+
703
+ def get_warmup(self, subscription_type: str) -> str | None:
704
+ """
705
+ Get the warmup period for a subscription type.
706
+
707
+ Args:
708
+ subscription_type: Type of subscription (e.g. DataType.OHLC["1h"], etc.)
709
+
710
+ Returns:
711
+ str: Warmup period or None if no warmup period is set
712
+ """
713
+ ...
714
+
715
+ def set_warmup(self, configs: dict[Any, str]) -> None:
716
+ """
717
+ Set the warmup period for different subscriptions.
718
+
719
+ If there are multiple ohlc configs specified, they will be warmed up in parallel.
720
+
721
+ Args:
722
+ configs: Dictionary of subscription types and warmup periods.
723
+ Keys can be subscription types of dictionaries with subscription parameters.
724
+
725
+ Example:
726
+ set_warmup({
727
+ DataType.OHLC["1h"]: "30d",
728
+ DataType.OHLC["1Min"]: "6h",
729
+ DataType.OHLC["1Sec"]: "5Min",
730
+ DataType.TRADE: "1h",
731
+ })
732
+ """
733
+ ...
734
+
735
+ def commit(self) -> None:
736
+ """
737
+ Apply all pending changes.
738
+ """
739
+ ...
740
+
741
+ @property
742
+ def auto_subscribe(self) -> bool:
743
+ """
744
+ Get whether new instruments are automatically subscribed to existing subscriptions.
745
+
746
+ Returns:
747
+ bool: True if auto-subscription is enabled
748
+ """
749
+ ...
750
+
751
+ @auto_subscribe.setter
752
+ def auto_subscribe(self, value: bool) -> None:
753
+ """
754
+ Enable or disable automatic subscription of new instruments.
755
+
756
+ Args:
757
+ value: True to enable auto-subscription, False to disable
758
+ """
759
+ ...
760
+
761
+
762
+ class IAccountProcessor(IAccountViewer):
763
+ time_provider: ITimeProvider
764
+
765
+ def start(self):
766
+ """
767
+ Start the account processor.
768
+ """
769
+ ...
770
+
771
+ def stop(self):
772
+ """
773
+ Stop the account processor.
774
+ """
775
+ ...
776
+
777
+ def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
778
+ """Set the subscription manager for the account processor.
779
+
780
+ Args:
781
+ manager: ISubscriptionManager instance to set
782
+ """
783
+ ...
784
+
785
+ def update_balance(self, currency: str, total: float, locked: float):
786
+ """Update balance for a specific currency.
787
+
788
+ Args:
789
+ currency: Currency code
790
+ total: Total amount of currency
791
+ locked: Amount of locked currency
792
+ """
793
+ ...
794
+
795
+ # TODO: refactor interface to accept float, Quote, Trade
796
+ def update_position_price(self, time: dt_64, instrument: Instrument, price: float) -> None:
797
+ """Update position price for an instrument.
798
+
799
+ Args:
800
+ time: Timestamp of the update
801
+ instrument: Instrument being updated
802
+ price: New price
803
+ """
804
+ ...
805
+
806
+ def process_deals(self, instrument: Instrument, deals: list[Deal]) -> None:
807
+ """Process executed deals for an instrument.
808
+
809
+ Args:
810
+ instrument: Instrument the deals belong to
811
+ deals: List of deals to process
812
+ """
813
+ ...
814
+
815
+ def process_order(self, order: Order) -> None:
816
+ """Process order updates.
817
+
818
+ Args:
819
+ order: Order to process
820
+ """
821
+ ...
822
+
823
+ def attach_positions(self, *position: Position) -> "IAccountProcessor":
824
+ """Attach positions to the account.
825
+
826
+ Args:
827
+ *position: Position objects to attach
828
+
829
+ Returns:
830
+ I"IAccountProcessor": Self for chaining
831
+ """
832
+ ...
833
+
834
+ def add_active_orders(self, orders: Dict[str, Order]) -> None:
835
+ """Add active orders to the account.
836
+
837
+ Warning only use in the beginning for state restoration because it does not update locked balances.
838
+
839
+ Args:
840
+ orders: Dictionary mapping order IDs to Order objects
841
+ """
842
+ ...
843
+
844
+
845
+ class IProcessingManager:
846
+ """Manages event processing."""
847
+
848
+ def process_data(self, instrument: Instrument, d_type: str, data: Any, is_historical: bool) -> bool:
849
+ """
850
+ Process incoming data.
851
+
852
+ Args:
853
+ instrument: Instrument the data is for
854
+ d_type: Type of the data
855
+ data: The data to process
856
+
857
+ Returns:
858
+ bool: True if processing should be halted
859
+ """
860
+ ...
861
+
862
+ def set_fit_schedule(self, schedule: str) -> None:
863
+ """
864
+ Set the schedule for fitting the strategy model (default is to trigger fit only at start).
865
+ """
866
+ ...
867
+
868
+ def set_event_schedule(self, schedule: str) -> None:
869
+ """
870
+ Set the schedule for triggering events (default is to only trigger on data events).
871
+ """
872
+ ...
873
+
874
+ def get_event_schedule(self, event_id: str) -> str | None:
875
+ """
876
+ Get defined schedule for event id.
877
+ """
878
+ ...
879
+
880
+ def is_fitted(self) -> bool:
881
+ """
882
+ Check if the strategy is fitted.
883
+ """
884
+ ...
885
+
886
+
887
+ class IStrategyContext(
888
+ IMarketManager,
889
+ ITradingManager,
890
+ IUniverseManager,
891
+ ISubscriptionManager,
892
+ IProcessingManager,
893
+ IAccountViewer,
894
+ ):
895
+ strategy: "IStrategy"
896
+
897
+ def start(self, blocking: bool = False):
898
+ """
899
+ Starts the strategy context.
900
+
901
+ Args:
902
+ blocking: Whether to block the main thread
903
+ """
904
+ ...
905
+
906
+ def stop(self):
907
+ """Stops the strategy context."""
908
+ ...
909
+
910
+ def is_running(self) -> bool:
911
+ """
912
+ Check if the strategy is running.
913
+ """
914
+ ...
915
+
916
+ @property
917
+ def is_simulation(self) -> bool:
918
+ """
919
+ Check if the strategy is running in simulation mode.
920
+ """
921
+ ...
922
+
923
+ @property
924
+ def exchanges(self) -> list[str]:
925
+ """
926
+ Returns a list of exchanges in this context. There is one exchange in the most cases.
927
+ """
928
+ ...
929
+
930
+
931
+ class IPositionGathering:
932
+ """
933
+ Common interface for position gathering
934
+ """
935
+
936
+ def alter_position_size(self, ctx: IStrategyContext, target: TargetPosition) -> float: ...
937
+
938
+ def alter_positions(
939
+ self, ctx: IStrategyContext, targets: List[TargetPosition] | TargetPosition
940
+ ) -> Dict[Instrument, float]:
941
+ if not isinstance(targets, list):
942
+ targets = [targets]
943
+
944
+ res = {}
945
+ if targets:
946
+ for t in targets:
947
+ if t.is_service: # we skip processing service positions
948
+ continue
949
+ try:
950
+ res[t.instrument] = self.alter_position_size(ctx, t)
951
+ except Exception as ex:
952
+ logger.error(f"[{ctx.time()}]: Failed processing target position {t} : {ex}")
953
+ logger.opt(colors=False).error(traceback.format_exc())
954
+ return res
955
+
956
+ def on_execution_report(self, ctx: IStrategyContext, instrument: Instrument, deal: Deal): ...
957
+
958
+
959
+ class IPositionSizer:
960
+ """Interface for calculating target positions from signals."""
961
+
962
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: list[Signal]) -> list[TargetPosition]:
963
+ """Calculates target position sizes.
964
+
965
+ Args:
966
+ ctx: Strategy context object.
967
+ signals: List of signals to process.
968
+
969
+ Returns:
970
+ List of target positions.
971
+ """
972
+ raise NotImplementedError("calculate_target_positions is not implemented")
973
+
974
+ def get_signal_entry_price(
975
+ self, ctx: IStrategyContext, signal: Signal, use_mid_price: bool = False
976
+ ) -> float | None:
977
+ """
978
+ Get the entry price for a signal.
979
+ """
980
+ _entry = None
981
+ if signal.price is not None and signal.price > 0:
982
+ _entry = signal.price
983
+ else:
984
+ if (_q := ctx.quote(signal.instrument)) is not None:
985
+ _entry = _q.mid_price() if use_mid_price else (_q.ask if np.sign(signal.signal) > 0 else _q.bid)
986
+ else:
987
+ logger.error(
988
+ f"{self.__class__.__name__}: Can't get actual market quote for {signal.instrument} and signal price is not set ({str(signal)}) !"
989
+ )
990
+
991
+ return _entry
992
+
993
+
994
+ class PositionsTracker:
995
+ """
996
+ Process signals from strategy and track position. It can contains logic for risk management for example.
997
+ """
998
+
999
+ _sizer: IPositionSizer
1000
+
1001
+ def __init__(self, sizer: IPositionSizer) -> None:
1002
+ self._sizer = sizer
1003
+
1004
+ def get_position_sizer(self) -> IPositionSizer:
1005
+ return self._sizer
1006
+
1007
+ def is_active(self, instrument: Instrument) -> bool:
1008
+ return True
1009
+
1010
+ def process_signals(self, ctx: IStrategyContext, signals: list[Signal]) -> list[TargetPosition] | TargetPosition:
1011
+ """
1012
+ Default implementation just returns calculated target positions
1013
+ """
1014
+ return self.get_position_sizer().calculate_target_positions(ctx, signals)
1015
+
1016
+ def update(
1017
+ self, ctx: IStrategyContext, instrument: Instrument, update: Timestamped
1018
+ ) -> List[TargetPosition] | TargetPosition:
1019
+ """
1020
+ Tracker is being updated by new market data.
1021
+ It may require to change position size or create new position because of interior tracker's logic (risk management for example).
1022
+ """
1023
+ ...
1024
+
1025
+ def on_execution_report(self, ctx: IStrategyContext, instrument: Instrument, deal: Deal):
1026
+ """
1027
+ Tracker is notified when execution report is received
1028
+ """
1029
+ ...
1030
+
1031
+
1032
+ def _unpickle_instance(chain: tuple[type], state: dict):
1033
+ """
1034
+ chain is a tuple of the *original* classes, e.g. (A, B, C).
1035
+ Reconstruct a new ephemeral class that inherits from them.
1036
+ """
1037
+ name = "_".join(cls.__name__ for cls in chain)
1038
+ # Reverse the chain to respect the typical left-to-right MRO
1039
+ inst = type(name, chain[::-1], {"__module__": "__main__"})()
1040
+ inst.__dict__.update(state)
1041
+ return inst
1042
+
1043
+
1044
+ class Mixable(type):
1045
+ """
1046
+ It's possible to create composite strategies dynamically by adding mixins with functionality.
1047
+
1048
+ NewStrategy = (SignalGenerator + RiskManager + PositionGathering)
1049
+ NewStrategy(....) can be used in simulation or live trading.
1050
+ """
1051
+
1052
+ def __add__(cls, other_cls):
1053
+ # If we already have a _composition, combine them;
1054
+ # else treat cls itself as the start of the chain
1055
+ cls_chain = getattr(cls, "__composition__", (cls,))
1056
+ other_chain = getattr(other_cls, "__composition__", (other_cls,))
1057
+
1058
+ # Combine them into one chain. You can define your own order rules:
1059
+ new_chain = cls_chain + other_chain
1060
+
1061
+ # Create ephemeral class
1062
+ name = "_".join(c.__name__ for c in new_chain)
1063
+
1064
+ def __reduce__(self):
1065
+ # Just return the chain of *original real classes*
1066
+ return _unpickle_instance, (new_chain, self.__dict__)
1067
+
1068
+ new_cls = type(
1069
+ name,
1070
+ new_chain[::-1],
1071
+ {"__module__": cls.__module__, "__composition__": new_chain, "__reduce__": __reduce__},
1072
+ )
1073
+ return new_cls
1074
+
1075
+
1076
+ class IStrategy(metaclass=Mixable):
1077
+ """Base class for trading strategies."""
1078
+
1079
+ ctx: IStrategyContext
1080
+
1081
+ def __init__(self, **kwargs) -> None:
1082
+ set_parameters_to_object(self, **kwargs)
1083
+
1084
+ def on_init(self, ctx: IStrategyContext):
1085
+ """
1086
+ This method is called when strategy is initialized.
1087
+ It is useful for setting the base subscription and warmup periods via the subscription manager.
1088
+ """
1089
+ ...
1090
+
1091
+ def on_start(self, ctx: IStrategyContext):
1092
+ """
1093
+ This method is called strategy is started. You can already use the market data provider.
1094
+ """
1095
+ pass
1096
+
1097
+ def on_fit(self, ctx: IStrategyContext):
1098
+ """
1099
+ Called when it's time to fit the model.
1100
+ """
1101
+ return None
1102
+
1103
+ def on_universe_change(
1104
+ self, ctx: IStrategyContext, add_instruments: list[Instrument], rm_instruments: list[Instrument]
1105
+ ) -> None:
1106
+ """
1107
+ This method is called when the trading universe is updated.
1108
+ """
1109
+ return None
1110
+
1111
+ def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> List[Signal] | Signal | None:
1112
+ """Called on strategy events.
1113
+
1114
+ Args:
1115
+ ctx: Strategy context.
1116
+ event: Trigger event to process.
1117
+
1118
+ Returns:
1119
+ List of signals, single signal, or None.
1120
+ """
1121
+ return None
1122
+
1123
+ def on_market_data(self, ctx: IStrategyContext, data: MarketEvent) -> List[Signal] | Signal | None:
1124
+ """
1125
+ Called when new market data is received.
1126
+
1127
+ Args:
1128
+ ctx: Strategy context.
1129
+ data: The market data received.
1130
+
1131
+ Returns:
1132
+ List of signals, single signal, or None.
1133
+ """
1134
+ return None
1135
+
1136
+ def on_order_update(self, ctx: IStrategyContext, order: Order) -> list[Signal] | Signal | None:
1137
+ """
1138
+ Called when an order update is received.
1139
+
1140
+ Args:
1141
+ ctx: Strategy context.
1142
+ order: The order update.
1143
+ """
1144
+ return None
1145
+
1146
+ def on_stop(self, ctx: IStrategyContext):
1147
+ pass
1148
+
1149
+ def tracker(self, ctx: IStrategyContext) -> PositionsTracker | None:
1150
+ pass