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,203 @@
1
+ from collections import defaultdict
2
+ from typing import Any
3
+
4
+ from qubx.core.basics import DataType, Instrument
5
+ from qubx.core.interfaces import IDataProvider, ISubscriptionManager
6
+ from qubx.utils.misc import synchronized
7
+
8
+
9
+ class SubscriptionManager(ISubscriptionManager):
10
+ _data_provider: IDataProvider
11
+ _base_sub: str
12
+ _sub_to_warmup: dict[str, str]
13
+ _auto_subscribe: bool
14
+
15
+ _pending_global_subscriptions: set[str]
16
+ _pending_global_unsubscriptions: set[str]
17
+
18
+ _pending_stream_subscriptions: dict[str, set[Instrument]]
19
+ _pending_stream_unsubscriptions: dict[str, set[Instrument]]
20
+ _pending_warmups: dict[tuple[str, Instrument], str]
21
+
22
+ def __init__(
23
+ self,
24
+ data_provider: IDataProvider,
25
+ auto_subscribe: bool = True,
26
+ default_base_subscription: DataType = DataType.NONE,
27
+ ) -> None:
28
+ self._data_provider = data_provider
29
+ self._base_sub = default_base_subscription
30
+ self._sub_to_warmup = {}
31
+ self._pending_warmups = {}
32
+ self._pending_global_subscriptions = set()
33
+ self._pending_global_unsubscriptions = set()
34
+ self._pending_stream_subscriptions = defaultdict(set)
35
+ self._pending_stream_unsubscriptions = defaultdict(set)
36
+ self._auto_subscribe = auto_subscribe
37
+
38
+ def subscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
39
+ # - figure out which instruments to subscribe to (all or specific)
40
+ if instruments is None:
41
+ self._pending_global_subscriptions.add(subscription_type)
42
+ return
43
+
44
+ if isinstance(instruments, Instrument):
45
+ instruments = [instruments]
46
+
47
+ # - get instruments that are not already subscribed to
48
+ _current_instruments = self._data_provider.get_subscribed_instruments(subscription_type)
49
+ instruments = list(set(instruments).difference(_current_instruments))
50
+
51
+ # - subscribe to all existing subscriptions if subscription_type is ALL
52
+ if subscription_type == DataType.ALL:
53
+ subscriptions = self.get_subscriptions()
54
+ for sub in subscriptions:
55
+ self.subscribe(sub, instruments)
56
+ return
57
+
58
+ self._pending_stream_subscriptions[subscription_type].update(instruments)
59
+ self._update_pending_warmups(subscription_type, instruments)
60
+
61
+ def unsubscribe(self, subscription_type: str, instruments: list[Instrument] | Instrument | None = None) -> None:
62
+ if instruments is None:
63
+ self._pending_global_unsubscriptions.add(subscription_type)
64
+ return
65
+
66
+ if isinstance(instruments, Instrument):
67
+ instruments = [instruments]
68
+
69
+ # - subscribe to all existing subscriptions if subscription_type is ALL
70
+ if subscription_type == DataType.ALL:
71
+ subscriptions = self.get_subscriptions()
72
+ for sub in subscriptions:
73
+ self.unsubscribe(sub, instruments)
74
+ return
75
+
76
+ self._pending_stream_unsubscriptions[subscription_type].update(instruments)
77
+
78
+ @synchronized
79
+ def commit(self) -> None:
80
+ if not self._has_operations_to_commit():
81
+ return
82
+
83
+ # - warm up subscriptions
84
+ self._run_warmup()
85
+
86
+ # - update subscriptions
87
+ for _sub in self._get_updated_subs():
88
+ _current_sub_instruments = set(self._data_provider.get_subscribed_instruments(_sub))
89
+ _removed_instruments = self._pending_stream_unsubscriptions.get(_sub, set())
90
+ _added_instruments = self._pending_stream_subscriptions.get(_sub, set())
91
+
92
+ if _sub in self._pending_global_unsubscriptions:
93
+ _removed_instruments.update(_current_sub_instruments)
94
+
95
+ if _sub in self._pending_global_subscriptions:
96
+ _added_instruments.update(self._data_provider.get_subscribed_instruments())
97
+
98
+ # - subscribe collection
99
+ _updated_instruments = _current_sub_instruments.union(_added_instruments).difference(_removed_instruments)
100
+ if _updated_instruments != _current_sub_instruments:
101
+ self._data_provider.subscribe(_sub, _updated_instruments, reset=True)
102
+
103
+ # - unsubscribe instruments
104
+ if _removed_instruments:
105
+ self._data_provider.unsubscribe(_sub, _removed_instruments)
106
+
107
+ # - clean up pending subs and unsubs
108
+ self._pending_stream_subscriptions.clear()
109
+ self._pending_stream_unsubscriptions.clear()
110
+ self._pending_global_subscriptions.clear()
111
+ self._pending_global_unsubscriptions.clear()
112
+
113
+ def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
114
+ return self._data_provider.has_subscription(instrument, subscription_type)
115
+
116
+ def get_subscriptions(self, instrument: Instrument | None = None) -> list[str]:
117
+ return list(
118
+ set(self._data_provider.get_subscriptions(instrument))
119
+ | {self.get_base_subscription()}
120
+ | self._pending_global_subscriptions
121
+ )
122
+
123
+ def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
124
+ return self._data_provider.get_subscribed_instruments(subscription_type)
125
+
126
+ def get_base_subscription(self) -> str:
127
+ return self._base_sub
128
+
129
+ def set_base_subscription(self, subscription_type: str) -> None:
130
+ self._base_sub = subscription_type
131
+
132
+ def get_warmup(self, subscription_type: str) -> str | None:
133
+ return self._sub_to_warmup.get(subscription_type)
134
+
135
+ def set_warmup(self, configs: dict[Any, str]) -> None:
136
+ for subscription_type, period in configs.items():
137
+ self._sub_to_warmup[subscription_type] = period
138
+
139
+ @property
140
+ def auto_subscribe(self) -> bool:
141
+ return self._auto_subscribe
142
+
143
+ @auto_subscribe.setter
144
+ def auto_subscribe(self, value: bool) -> None:
145
+ self._auto_subscribe = value
146
+
147
+ def _get_updated_subs(self) -> list[str]:
148
+ return list(
149
+ set(self._pending_stream_unsubscriptions.keys())
150
+ | set(self._pending_stream_subscriptions.keys())
151
+ | self._pending_global_subscriptions
152
+ | self._pending_global_unsubscriptions
153
+ )
154
+
155
+ def _has_operations_to_commit(self) -> bool:
156
+ return any(
157
+ (
158
+ self._pending_stream_unsubscriptions,
159
+ self._pending_stream_subscriptions,
160
+ self._pending_global_subscriptions,
161
+ self._pending_global_unsubscriptions,
162
+ )
163
+ )
164
+
165
+ def _update_pending_warmups(self, subscription_type: str, instruments: list[Instrument]) -> None:
166
+ # TODO: refactor pending warmups in a way that would allow to subscribe and then call set_warmup in the same iteration
167
+ # - ohlc is handled separately
168
+ if DataType.from_str(subscription_type) != DataType.OHLC:
169
+ _warmup_period = self._sub_to_warmup.get(subscription_type)
170
+ if _warmup_period is not None:
171
+ for instrument in instruments:
172
+ self._pending_warmups[(subscription_type, instrument)] = _warmup_period
173
+
174
+ # - if base subscription, then we need to fetch historical OHLC data for warmup
175
+ if subscription_type == self._base_sub:
176
+ self._pending_warmups.update(
177
+ {
178
+ (sub, instrument): period
179
+ for sub, period in self._sub_to_warmup.items()
180
+ for instrument in instruments
181
+ if DataType.OHLC == sub
182
+ }
183
+ )
184
+
185
+ def _run_warmup(self) -> None:
186
+ # - handle warmup for global subscriptions
187
+ _subscribed_instruments = set(self._data_provider.get_subscribed_instruments())
188
+ _new_instruments = (
189
+ set.union(*self._pending_stream_subscriptions.values()) if self._pending_stream_subscriptions else set()
190
+ )
191
+
192
+ for sub in self._pending_global_subscriptions:
193
+ _warmup_period = self._sub_to_warmup.get(sub)
194
+ if _warmup_period is None:
195
+ continue
196
+ _sub_instruments = self._data_provider.get_subscribed_instruments(sub)
197
+ _add_instruments = _subscribed_instruments.union(_new_instruments).difference(_sub_instruments)
198
+ for instr in _add_instruments:
199
+ self._pending_warmups[(sub, instr)] = _warmup_period
200
+
201
+ # TODO: think about appropriate handling of timeouts
202
+ self._data_provider.warmup(self._pending_warmups.copy())
203
+ self._pending_warmups.clear()
@@ -0,0 +1,88 @@
1
+ from qubx import logger
2
+ from qubx.core.basics import Instrument, Order, OrderRequest
3
+ from qubx.core.interfaces import IAccountProcessor, IBroker, ITimeProvider, ITradingManager
4
+
5
+
6
+ class TradingManager(ITradingManager):
7
+ _time_provider: ITimeProvider
8
+ _broker: IBroker
9
+ _account: IAccountProcessor
10
+ _strategy_name: str
11
+
12
+ _order_id: int | None = None
13
+
14
+ def __init__(
15
+ self, time_provider: ITimeProvider, broker: IBroker, account: IAccountProcessor, strategy_name: str
16
+ ) -> None:
17
+ self._time_provider = time_provider
18
+ self._broker = broker
19
+ self._account = account
20
+ self._strategy_name = strategy_name
21
+
22
+ def trade(
23
+ self,
24
+ instrument: Instrument,
25
+ amount: float,
26
+ price: float | None = None,
27
+ time_in_force="gtc",
28
+ **options,
29
+ ) -> Order:
30
+ # - adjust size
31
+ size_adj = instrument.round_size_down(abs(amount))
32
+ if size_adj < instrument.min_size:
33
+ raise ValueError(f"Attempt to trade size {abs(amount)} less than minimal allowed {instrument.min_size} !")
34
+
35
+ side = "buy" if amount > 0 else "sell"
36
+ type = "market"
37
+ if price is not None:
38
+ price = instrument.round_price_down(price) if amount > 0 else instrument.round_price_up(price)
39
+ type = "limit"
40
+ if (stp_type := options.get("stop_type")) is not None:
41
+ type = f"stop_{stp_type}"
42
+
43
+ client_id = self._generate_order_client_id(instrument.symbol)
44
+ logger.debug(
45
+ f" [<y>{self.__class__.__name__}</y>(<g>{instrument.symbol}</g>)] :: Sending {type} {side} {size_adj} { ' @ ' + str(price) if price else ''} -> (client_id: <r>{client_id})</r> ..."
46
+ )
47
+
48
+ order = self._broker.send_order(
49
+ instrument=instrument,
50
+ order_side=side,
51
+ order_type=type,
52
+ amount=size_adj,
53
+ price=price,
54
+ time_in_force=time_in_force,
55
+ client_id=client_id,
56
+ **options,
57
+ )
58
+ return order
59
+
60
+ def submit_orders(self, order_requests: list[OrderRequest]) -> list[Order]:
61
+ raise NotImplementedError("Not implemented yet")
62
+
63
+ def set_target_position(
64
+ self, instrument: Instrument, target: float, price: float | None = None, time_in_force="gtc", **options
65
+ ) -> Order:
66
+ raise NotImplementedError("Not implemented yet")
67
+
68
+ def close_position(self, instrument: Instrument) -> None:
69
+ raise NotImplementedError("Not implemented yet")
70
+
71
+ def cancel_order(self, order_id: str) -> None:
72
+ if not order_id:
73
+ return
74
+ self._broker.cancel_order(order_id)
75
+
76
+ def cancel_orders(self, instrument: Instrument) -> None:
77
+ for o in self._account.get_orders(instrument).values():
78
+ self._broker.cancel_order(o.id)
79
+
80
+ def _generate_order_client_id(self, symbol: str) -> str:
81
+ if self._order_id is None:
82
+ self._order_id = self._time_provider.time().astype("int64") // 100_000_000
83
+ assert self._order_id is not None
84
+ self._order_id += 1
85
+ return "_".join(["qubx", symbol, str(self._order_id)])
86
+
87
+ def exchanges(self) -> list[str]:
88
+ return [self._broker.exchange()]
@@ -0,0 +1,270 @@
1
+ from qubx.core.basics import DataType, Instrument, TargetPosition
2
+ from qubx.core.helpers import CachedMarketDataHolder
3
+ from qubx.core.interfaces import (
4
+ IAccountProcessor,
5
+ IBroker,
6
+ IDataProvider,
7
+ IPositionGathering,
8
+ IStrategy,
9
+ IStrategyContext,
10
+ ISubscriptionManager,
11
+ ITimeProvider,
12
+ ITradingManager,
13
+ IUniverseManager,
14
+ RemovalPolicy,
15
+ )
16
+ from qubx.core.loggers import StrategyLogging
17
+
18
+
19
+ class UniverseManager(IUniverseManager):
20
+ _context: IStrategyContext
21
+ _strategy: IStrategy
22
+ _broker: IDataProvider
23
+ _trading_service: IBroker
24
+ _cache: CachedMarketDataHolder
25
+ _logging: StrategyLogging
26
+ _subscription_manager: ISubscriptionManager
27
+ _trading_manager: ITradingManager
28
+ _time_provider: ITimeProvider
29
+ _account: IAccountProcessor
30
+ _position_gathering: IPositionGathering
31
+ _removal_queue: dict[Instrument, tuple[RemovalPolicy, bool]]
32
+
33
+ def __init__(
34
+ self,
35
+ context: IStrategyContext,
36
+ strategy: IStrategy,
37
+ broker: IDataProvider,
38
+ trading_service: IBroker,
39
+ cache: CachedMarketDataHolder,
40
+ logging: StrategyLogging,
41
+ subscription_manager: ISubscriptionManager,
42
+ trading_manager: ITradingManager,
43
+ time_provider: ITimeProvider,
44
+ account: IAccountProcessor,
45
+ position_gathering: IPositionGathering,
46
+ ):
47
+ self._context = context
48
+ self._strategy = strategy
49
+ self._broker = broker
50
+ self._trading_service = trading_service
51
+ self._cache = cache
52
+ self._logging = logging
53
+ self._subscription_manager = subscription_manager
54
+ self._trading_manager = trading_manager
55
+ self._time_provider = time_provider
56
+ self._account = account
57
+ self._position_gathering = position_gathering
58
+ self._instruments = []
59
+ self._removal_queue = {}
60
+
61
+ def _has_position(self, instrument: Instrument) -> bool:
62
+ return (
63
+ instrument in self._account.positions
64
+ and abs(self._account.positions[instrument].quantity) > instrument.min_size
65
+ )
66
+
67
+ def set_universe(
68
+ self,
69
+ instruments: list[Instrument],
70
+ skip_callback: bool = False,
71
+ if_has_position_then: RemovalPolicy = "close",
72
+ ) -> None:
73
+ assert if_has_position_then in (
74
+ "close",
75
+ "wait_for_close",
76
+ "wait_for_change",
77
+ ), "Invalid if_has_position_then policy"
78
+
79
+ new_set = set(instruments)
80
+ prev_set = set(self._instruments)
81
+
82
+ # - determine instruments to remove depending on if_has_position_then policy
83
+ may_be_removed = list(prev_set - new_set)
84
+
85
+ # - split instruments into removable and keepable
86
+ to_remove, to_keep = self._get_what_can_be_removed_or_kept(may_be_removed, skip_callback, if_has_position_then)
87
+
88
+ to_add = list(new_set - prev_set)
89
+ self.__do_add_instruments(to_add)
90
+ self.__do_remove_instruments(to_remove)
91
+
92
+ # - cleanup removal queue
93
+ self.__cleanup_removal_queue(instruments)
94
+
95
+ if not skip_callback and (to_add or to_remove):
96
+ self._strategy.on_universe_change(self._context, to_add, to_remove)
97
+
98
+ self._subscription_manager.commit() # apply pending changes
99
+
100
+ # set new instruments
101
+ self._instruments.clear()
102
+ self._instruments.extend(instruments)
103
+ self._instruments.extend(to_keep)
104
+
105
+ def _get_what_can_be_removed_or_kept(
106
+ self, may_be_removed: list[Instrument], skip_callback: bool, if_has_position_then: RemovalPolicy
107
+ ) -> tuple[list[Instrument], list[Instrument]]:
108
+ immediately_close = if_has_position_then == "close"
109
+ to_remove, to_keep = [], []
110
+ for instr in may_be_removed:
111
+ if immediately_close:
112
+ to_remove.append(instr)
113
+ else:
114
+ if self._has_position(instr):
115
+ self._removal_queue[instr] = (if_has_position_then, skip_callback)
116
+ to_keep.append(instr)
117
+ return to_remove, to_keep
118
+
119
+ def __cleanup_removal_queue(self, instruments: list[Instrument]):
120
+ for instr in instruments:
121
+ # - if it's still in the removal queue, remove it
122
+ if instr in self._removal_queue:
123
+ self._removal_queue.pop(instr)
124
+
125
+ def add_instruments(self, instruments: list[Instrument]):
126
+ self.__do_add_instruments(instruments)
127
+ self.__cleanup_removal_queue(instruments)
128
+ self._strategy.on_universe_change(self._context, instruments, [])
129
+ self._subscription_manager.commit()
130
+ self._instruments.extend(instruments)
131
+
132
+ def remove_instruments(
133
+ self,
134
+ instruments: list[Instrument],
135
+ if_has_position_then: RemovalPolicy = "close",
136
+ ):
137
+ assert if_has_position_then in (
138
+ "close",
139
+ "wait_for_close",
140
+ "wait_for_change",
141
+ ), "Invalid if_has_position_then policy"
142
+
143
+ # - split instruments into removable and keepable
144
+ to_remove, to_keep = self._get_what_can_be_removed_or_kept(instruments, False, if_has_position_then)
145
+
146
+ # - remove ones that can be removed immediately
147
+ self.__do_remove_instruments(to_remove)
148
+ self._strategy.on_universe_change(self._context, [], to_remove)
149
+ self._subscription_manager.commit()
150
+
151
+ # - update instruments list
152
+ self._instruments = list(set(self._instruments) - set(to_remove))
153
+ self._instruments.extend(to_keep)
154
+
155
+ @property
156
+ def instruments(self) -> list[Instrument]:
157
+ return self._instruments
158
+
159
+ def __do_remove_instruments(self, instruments: list[Instrument]):
160
+ """
161
+ Remove symbols from universe. Steps:
162
+ - [v] cancel all open orders
163
+ - [v] close all open positions
164
+ - [v] unsubscribe from market data
165
+ - [v] remove from data cache
166
+
167
+ We are still keeping the symbols in the positions dictionary.
168
+ """
169
+ if not instruments:
170
+ return
171
+
172
+ # - preprocess instruments and cancel all open orders
173
+ for instr in instruments:
174
+ # - remove instrument from the removal queue if it's there
175
+ self._removal_queue.pop(instr, None)
176
+
177
+ # - cancel all open orders
178
+ self._trading_manager.cancel_orders(instr)
179
+
180
+ # - close all open positions
181
+ exit_targets = [
182
+ TargetPosition.zero(self._context, instr.signal(0, group="Universe", comment="Universe change"))
183
+ for instr in instruments
184
+ if self._has_position(instr)
185
+ ]
186
+ self._position_gathering.alter_positions(self._context, exit_targets)
187
+
188
+ # - if still open positions close them manually
189
+ for instr in instruments:
190
+ pos = self._account.positions.get(instr)
191
+ if pos and abs(pos.quantity) > instr.min_size:
192
+ self._trading_manager.trade(instr, -pos.quantity)
193
+
194
+ # - unsubscribe from market data
195
+ for instr in instruments:
196
+ self._subscription_manager.unsubscribe(DataType.ALL, instr)
197
+
198
+ # - remove from data cache
199
+ for instr in instruments:
200
+ self._cache.remove(instr)
201
+
202
+ def __do_add_instruments(self, instruments: list[Instrument]) -> None:
203
+ # - create positions for instruments
204
+ self._create_and_update_positions(instruments)
205
+
206
+ # - initialize ohlcv for new instruments
207
+ for instr in instruments:
208
+ self._cache.init_ohlcv(instr)
209
+
210
+ # - subscribe to market data
211
+ self._subscription_manager.subscribe(
212
+ (
213
+ DataType.ALL
214
+ if self._subscription_manager.auto_subscribe
215
+ else self._subscription_manager.get_base_subscription()
216
+ ),
217
+ instruments,
218
+ )
219
+
220
+ # - reinitialize strategy loggers
221
+ self._logging.initialize(self._time_provider.time(), self._account.positions, self._account.get_balances())
222
+
223
+ def _create_and_update_positions(self, instruments: list[Instrument]):
224
+ for instrument in instruments:
225
+ _ = self._account.get_position(instrument)
226
+
227
+ # - check if we need any aux instrument for calculating pnl ?
228
+ # TODO: test edge cases for aux symbols (UniverseManager)
229
+ # aux = lookup.find_aux_instrument_for(instrument, self._account.get_base_currency())
230
+ # if aux is not None:
231
+ # instrument._aux_instrument = aux
232
+ # instruments.append(aux)
233
+ # _ = self._trading_service.get_position(aux)
234
+
235
+ def on_alter_position(self, instrument: Instrument) -> None:
236
+ """
237
+ Called when the position of an instrument changes.
238
+ It can be used for postponed unsubscribed events
239
+ """
240
+ # - check if need to remove instrument from the universe
241
+ if instrument in self._removal_queue:
242
+ _, skip_callback = self._removal_queue[instrument]
243
+
244
+ # - if no position, remove instrument from the universe
245
+ if not self._has_position(instrument):
246
+ self.__do_remove_instruments([instrument])
247
+
248
+ if not skip_callback:
249
+ self._strategy.on_universe_change(self._context, [], [instrument])
250
+
251
+ # - commit changes and remove instrument from the universe
252
+ self._subscription_manager.commit()
253
+ self._instruments.remove(instrument)
254
+
255
+ def is_trading_allowed(self, instrument: Instrument) -> bool:
256
+ if instrument in self._removal_queue:
257
+ policy, skip_callback = self._removal_queue[instrument]
258
+
259
+ if policy == "wait_for_change":
260
+ self.__do_remove_instruments([instrument])
261
+
262
+ if not skip_callback:
263
+ self._strategy.on_universe_change(self._context, [], [instrument])
264
+
265
+ # - commit changes and remove instrument from the universe
266
+ self._subscription_manager.commit()
267
+ self._instruments.remove(instrument)
268
+ return False
269
+
270
+ return True
qubx/core/series.pxd ADDED
@@ -0,0 +1,125 @@
1
+ import numpy as np
2
+ cimport numpy as np
3
+
4
+
5
+ cdef np.ndarray nans(int dims)
6
+
7
+
8
+ cdef class Indexed:
9
+ cdef public list values
10
+ cdef public float max_series_length
11
+ cdef unsigned short _is_empty
12
+
13
+
14
+ cdef class Locator:
15
+ cdef TimeSeries _series
16
+
17
+
18
+ cdef class TimeSeries:
19
+ cdef public long long timeframe
20
+ cdef public Indexed times
21
+ cdef public Indexed values
22
+ cdef public float max_series_length
23
+ cdef public Locator loc
24
+ cdef unsigned short _is_new_item
25
+ cdef public str name
26
+ cdef dict indicators # it's used for indicators caching
27
+ cdef list calculation_order # calculation order as list: [ (input_id, indicator_obj, indicator_id) ]
28
+ cdef double _process_every_update
29
+ cdef double _last_bar_update_value
30
+ cdef long long _last_bar_update_time
31
+
32
+ cdef _update_indicators(TimeSeries self, long long time, object value, short new_item_started)
33
+
34
+
35
+ cdef class Indicator(TimeSeries):
36
+ cdef public TimeSeries series
37
+ cdef public TimeSeries parent
38
+
39
+
40
+ cdef class RollingSum:
41
+ """
42
+ Rolling fast summator
43
+ """
44
+ cdef unsigned int period
45
+ cdef np.ndarray __s
46
+ cdef unsigned int __i
47
+ cdef double rsum
48
+ cdef public unsigned short is_init_stage
49
+
50
+ cpdef double update(RollingSum self, double value, short new_item_started)
51
+
52
+
53
+ cdef class Bar:
54
+ cdef public long long time
55
+ cdef public double open
56
+ cdef public double high
57
+ cdef public double low
58
+ cdef public double close
59
+ cdef public double volume # total volume (in quote asset)
60
+ cdef public double bought_volume # volume bought (in quote asset) if presented
61
+
62
+ cpdef Bar update(Bar self, double price, double volume, double bought_volume=*)
63
+
64
+ cpdef dict to_dict(Bar self, unsigned short skip_time=*)
65
+
66
+
67
+ cdef class OHLCV(TimeSeries):
68
+ cdef public TimeSeries open
69
+ cdef public TimeSeries high
70
+ cdef public TimeSeries low
71
+ cdef public TimeSeries close
72
+ cdef public TimeSeries volume
73
+ cdef public TimeSeries bvolume
74
+
75
+ cpdef short update(OHLCV self, long long time, double price, double volume=*, double bvolume=*)
76
+
77
+ cpdef short update_by_bar(OHLCV self, long long time, double open, double high, double low, double close, double vol_incr=*, double b_vol_incr=*)
78
+
79
+ cpdef _update_indicators(OHLCV self, long long time, object value, short new_item_started)
80
+
81
+ cpdef object append_data(
82
+ OHLCV self,
83
+ np.ndarray times,
84
+ np.ndarray opens,
85
+ np.ndarray highs,
86
+ np.ndarray lows,
87
+ np.ndarray closes,
88
+ np.ndarray volumes,
89
+ np.ndarray bvolumes
90
+ )
91
+
92
+
93
+ cdef class Trade:
94
+ cdef public long long time
95
+ cdef public double price
96
+ cdef public double size
97
+ cdef public short taker
98
+ cdef public long long trade_id
99
+
100
+
101
+ cdef class Quote:
102
+ cdef public long long time
103
+ cdef public double bid
104
+ cdef public double ask
105
+ cdef public double bid_size
106
+ cdef public double ask_size
107
+
108
+ cpdef double mid_price(Quote self)
109
+
110
+
111
+ cdef class OrderBook:
112
+ cdef public long long time
113
+ cdef public double top_bid
114
+ cdef public double top_ask
115
+ cdef public double tick_size
116
+ cdef public np.ndarray bids
117
+ cdef public np.ndarray asks
118
+
119
+ cpdef Quote to_quote(OrderBook self)
120
+ cpdef double mid_price(OrderBook self)
121
+
122
+
123
+ cdef class IndicatorOHLC(Indicator):
124
+ pass
125
+