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.
- qubx/__init__.py +207 -0
- qubx/_nb_magic.py +100 -0
- qubx/backtester/__init__.py +5 -0
- qubx/backtester/account.py +145 -0
- qubx/backtester/broker.py +87 -0
- qubx/backtester/data.py +296 -0
- qubx/backtester/management.py +378 -0
- qubx/backtester/ome.py +296 -0
- qubx/backtester/optimization.py +201 -0
- qubx/backtester/simulated_data.py +558 -0
- qubx/backtester/simulator.py +362 -0
- qubx/backtester/utils.py +780 -0
- qubx/cli/__init__.py +0 -0
- qubx/cli/commands.py +67 -0
- qubx/connectors/ccxt/__init__.py +0 -0
- qubx/connectors/ccxt/account.py +495 -0
- qubx/connectors/ccxt/broker.py +132 -0
- qubx/connectors/ccxt/customizations.py +193 -0
- qubx/connectors/ccxt/data.py +612 -0
- qubx/connectors/ccxt/exceptions.py +17 -0
- qubx/connectors/ccxt/factory.py +93 -0
- qubx/connectors/ccxt/utils.py +307 -0
- qubx/core/__init__.py +0 -0
- qubx/core/account.py +251 -0
- qubx/core/basics.py +850 -0
- qubx/core/context.py +420 -0
- qubx/core/exceptions.py +38 -0
- qubx/core/helpers.py +480 -0
- qubx/core/interfaces.py +1150 -0
- qubx/core/loggers.py +514 -0
- qubx/core/lookups.py +475 -0
- qubx/core/metrics.py +1512 -0
- qubx/core/mixins/__init__.py +13 -0
- qubx/core/mixins/market.py +94 -0
- qubx/core/mixins/processing.py +428 -0
- qubx/core/mixins/subscription.py +203 -0
- qubx/core/mixins/trading.py +88 -0
- qubx/core/mixins/universe.py +270 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +125 -0
- qubx/core/series.pyi +118 -0
- qubx/core/series.pyx +988 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyi +6 -0
- qubx/core/utils.pyx +62 -0
- qubx/data/__init__.py +25 -0
- qubx/data/helpers.py +416 -0
- qubx/data/readers.py +1562 -0
- qubx/data/tardis.py +100 -0
- qubx/gathering/simplest.py +88 -0
- qubx/math/__init__.py +3 -0
- qubx/math/stats.py +129 -0
- qubx/pandaz/__init__.py +23 -0
- qubx/pandaz/ta.py +2757 -0
- qubx/pandaz/utils.py +638 -0
- qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx/resources/instruments/symbols-binance.json +1 -0
- qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +149 -0
- qubx/ta/indicators.pyi +41 -0
- qubx/ta/indicators.pyx +787 -0
- qubx/trackers/__init__.py +3 -0
- qubx/trackers/abvanced.py +236 -0
- qubx/trackers/composite.py +146 -0
- qubx/trackers/rebalancers.py +129 -0
- qubx/trackers/riskctrl.py +641 -0
- qubx/trackers/sizers.py +235 -0
- qubx/utils/__init__.py +5 -0
- qubx/utils/_pyxreloader.py +281 -0
- qubx/utils/charting/lookinglass.py +1057 -0
- qubx/utils/charting/mpl_helpers.py +1183 -0
- qubx/utils/marketdata/binance.py +284 -0
- qubx/utils/marketdata/ccxt.py +90 -0
- qubx/utils/marketdata/dukas.py +130 -0
- qubx/utils/misc.py +541 -0
- qubx/utils/ntp.py +63 -0
- qubx/utils/numbers_utils.py +7 -0
- qubx/utils/orderbook.py +491 -0
- qubx/utils/plotting/__init__.py +0 -0
- qubx/utils/plotting/dashboard.py +150 -0
- qubx/utils/plotting/data.py +137 -0
- qubx/utils/plotting/interfaces.py +25 -0
- qubx/utils/plotting/renderers/__init__.py +0 -0
- qubx/utils/plotting/renderers/plotly.py +0 -0
- qubx/utils/runner/__init__.py +1 -0
- qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx/utils/runner/accounts.py +88 -0
- qubx/utils/runner/configs.py +65 -0
- qubx/utils/runner/runner.py +470 -0
- qubx/utils/time.py +312 -0
- qubx-0.5.7.dist-info/METADATA +105 -0
- qubx-0.5.7.dist-info/RECORD +100 -0
- qubx-0.5.7.dist-info/WHEEL +4 -0
- 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
|
|
Binary file
|
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
|
+
|