walrasquant-lib 0.4.20__py3-none-any.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.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,819 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
from typing import Dict, List, cast
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from walrasquant.base import PublicConnector, PrivateConnector
|
|
5
|
+
from walrasquant.config import OrderQueryConfig
|
|
6
|
+
from walrasquant.core.nautilius_core import MessageBus, LiveClock
|
|
7
|
+
from walrasquant.core.entity import TaskManager
|
|
8
|
+
from walrasquant.core.registry import OrderRegistry
|
|
9
|
+
from walrasquant.core.cache import AsyncCache
|
|
10
|
+
from walrasquant.schema import (
|
|
11
|
+
BookL1,
|
|
12
|
+
Trade,
|
|
13
|
+
Kline,
|
|
14
|
+
BookL2,
|
|
15
|
+
BookOrderData,
|
|
16
|
+
FundingRate,
|
|
17
|
+
IndexPrice,
|
|
18
|
+
MarkPrice,
|
|
19
|
+
KlineList,
|
|
20
|
+
Ticker,
|
|
21
|
+
BaseMarket,
|
|
22
|
+
)
|
|
23
|
+
from walrasquant.constants import (
|
|
24
|
+
KlineInterval,
|
|
25
|
+
BookLevel,
|
|
26
|
+
)
|
|
27
|
+
from walrasquant.exchange.bybit.schema import (
|
|
28
|
+
BybitKlineResponse,
|
|
29
|
+
BybitKlineResponseArray,
|
|
30
|
+
BybitWsMessageGeneral,
|
|
31
|
+
BybitWsOrderbookDepthMsg,
|
|
32
|
+
BybitOrderBook,
|
|
33
|
+
BybitWsTradeMsg,
|
|
34
|
+
BybitWsTickerMsg,
|
|
35
|
+
BybitWsKlineMsg,
|
|
36
|
+
BybitTicker,
|
|
37
|
+
BybitIndexKlineResponse,
|
|
38
|
+
BybitIndexKlineResponseArray,
|
|
39
|
+
)
|
|
40
|
+
from walrasquant.exchange.bybit.rest_api import BybitApiClient
|
|
41
|
+
from walrasquant.exchange.bybit.websockets import BybitWSClient
|
|
42
|
+
from walrasquant.exchange.bybit.oms import BybitOrderManagementSystem
|
|
43
|
+
from walrasquant.exchange.bybit.constants import (
|
|
44
|
+
BybitAccountType,
|
|
45
|
+
BybitEnumParser,
|
|
46
|
+
)
|
|
47
|
+
from walrasquant.exchange.bybit.exchange import BybitExchangeManager
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BybitPublicConnector(PublicConnector):
|
|
51
|
+
_api_client: BybitApiClient
|
|
52
|
+
_ws_client: BybitWSClient
|
|
53
|
+
_account_type: BybitAccountType
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
account_type: BybitAccountType,
|
|
58
|
+
exchange: BybitExchangeManager,
|
|
59
|
+
msgbus: MessageBus,
|
|
60
|
+
clock: LiveClock,
|
|
61
|
+
task_manager: TaskManager,
|
|
62
|
+
custom_url: str | None = None,
|
|
63
|
+
enable_rate_limit: bool = True,
|
|
64
|
+
max_subscriptions_per_client: int | None = None,
|
|
65
|
+
max_clients: int | None = None,
|
|
66
|
+
):
|
|
67
|
+
if account_type in {BybitAccountType.UNIFIED, BybitAccountType.UNIFIED_TESTNET}:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
"Please not using `BybitAccountType.UNIFIED` or `BybitAccountType.UNIFIED_TESTNET` in `PublicConnector`"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
super().__init__(
|
|
73
|
+
account_type=account_type,
|
|
74
|
+
market=exchange.market,
|
|
75
|
+
market_id=exchange.market_id,
|
|
76
|
+
exchange_id=exchange.exchange_id,
|
|
77
|
+
ws_client=BybitWSClient(
|
|
78
|
+
account_type=account_type,
|
|
79
|
+
handler=self._ws_msg_handler,
|
|
80
|
+
clock=clock,
|
|
81
|
+
task_manager=task_manager,
|
|
82
|
+
custom_url=custom_url,
|
|
83
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
84
|
+
max_clients=max_clients,
|
|
85
|
+
),
|
|
86
|
+
clock=clock,
|
|
87
|
+
msgbus=msgbus,
|
|
88
|
+
api_client=BybitApiClient(
|
|
89
|
+
clock=clock,
|
|
90
|
+
testnet=account_type.is_testnet,
|
|
91
|
+
enable_rate_limit=enable_rate_limit,
|
|
92
|
+
),
|
|
93
|
+
task_manager=task_manager,
|
|
94
|
+
)
|
|
95
|
+
self._ws_msg_trade_decoder = msgspec.json.Decoder(BybitWsTradeMsg)
|
|
96
|
+
self._ws_msg_orderbook_decoder = msgspec.json.Decoder(BybitWsOrderbookDepthMsg)
|
|
97
|
+
self._ws_msg_general_decoder = msgspec.json.Decoder(BybitWsMessageGeneral)
|
|
98
|
+
self._ws_msg_kline_decoder = msgspec.json.Decoder(BybitWsKlineMsg)
|
|
99
|
+
self._ws_msg_ticker_decoder = msgspec.json.Decoder(BybitWsTickerMsg)
|
|
100
|
+
self._bookl1_orderbook = defaultdict(BybitOrderBook)
|
|
101
|
+
self._bookl2_orderbook = defaultdict(BybitOrderBook)
|
|
102
|
+
self._ticker: Dict[str, BybitTicker] = defaultdict(BybitTicker)
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def market_type(self):
|
|
106
|
+
if self._account_type.is_spot:
|
|
107
|
+
return "_spot"
|
|
108
|
+
elif self._account_type.is_linear:
|
|
109
|
+
return "_linear"
|
|
110
|
+
elif self._account_type.is_inverse:
|
|
111
|
+
return "_inverse"
|
|
112
|
+
else:
|
|
113
|
+
raise ValueError(f"Unsupported BybitAccountType.{self._account_type.value}")
|
|
114
|
+
|
|
115
|
+
def _get_category(self, market: BaseMarket):
|
|
116
|
+
if market.spot:
|
|
117
|
+
return "spot"
|
|
118
|
+
elif market.linear:
|
|
119
|
+
return "linear"
|
|
120
|
+
elif market.inverse:
|
|
121
|
+
return "inverse"
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"Unsupported market type: {market.type}")
|
|
124
|
+
|
|
125
|
+
def _ws_msg_handler(self, raw: bytes):
|
|
126
|
+
try:
|
|
127
|
+
ws_msg: BybitWsMessageGeneral = self._ws_msg_general_decoder.decode(raw)
|
|
128
|
+
# if ws_msg.ret_msg == "pong":
|
|
129
|
+
# self._ws_client._transport.notify_user_specific_pong_received()
|
|
130
|
+
# self._log.debug(f"Pong received {str(ws_msg)}")
|
|
131
|
+
# return
|
|
132
|
+
if ws_msg.success is False:
|
|
133
|
+
self._log.error(f"WebSocket error: {ws_msg}")
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
if "orderbook.1" in ws_msg.topic:
|
|
137
|
+
self._handle_orderbook(raw)
|
|
138
|
+
elif ws_msg.topic.startswith("orderbook."):
|
|
139
|
+
self._handle_orderbook_depth(raw)
|
|
140
|
+
elif "publicTrade" in ws_msg.topic:
|
|
141
|
+
self._handle_trade(raw)
|
|
142
|
+
elif "kline" in ws_msg.topic:
|
|
143
|
+
self._handle_kline(raw)
|
|
144
|
+
elif "tickers" in ws_msg.topic:
|
|
145
|
+
self._handle_ticker(raw)
|
|
146
|
+
except msgspec.DecodeError as e:
|
|
147
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
148
|
+
|
|
149
|
+
def _handle_ticker(self, raw: bytes):
|
|
150
|
+
msg: BybitWsTickerMsg = self._ws_msg_ticker_decoder.decode(raw)
|
|
151
|
+
id = msg.data.symbol + self.market_type
|
|
152
|
+
symbol = self._market_id[id]
|
|
153
|
+
|
|
154
|
+
ticker = self._ticker[symbol]
|
|
155
|
+
ticker.parse_ticker(msg)
|
|
156
|
+
funding_rate = ticker.fundingRate
|
|
157
|
+
next_funding_time = ticker.nextFundingTime
|
|
158
|
+
index_price = ticker.indexPrice
|
|
159
|
+
mark_price = ticker.markPrice
|
|
160
|
+
if (
|
|
161
|
+
funding_rate is None
|
|
162
|
+
or next_funding_time is None
|
|
163
|
+
or index_price is None
|
|
164
|
+
or mark_price is None
|
|
165
|
+
):
|
|
166
|
+
raise ValueError(f"Incomplete ticker payload for {symbol}")
|
|
167
|
+
|
|
168
|
+
funding_rate_msg = FundingRate(
|
|
169
|
+
exchange=self._exchange_id,
|
|
170
|
+
symbol=symbol,
|
|
171
|
+
rate=float(funding_rate),
|
|
172
|
+
timestamp=msg.ts,
|
|
173
|
+
next_funding_time=int(next_funding_time),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
index_price_msg = IndexPrice(
|
|
177
|
+
exchange=self._exchange_id,
|
|
178
|
+
symbol=symbol,
|
|
179
|
+
price=float(index_price),
|
|
180
|
+
timestamp=msg.ts,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
mark_price_msg = MarkPrice(
|
|
184
|
+
exchange=self._exchange_id,
|
|
185
|
+
symbol=symbol,
|
|
186
|
+
price=float(mark_price),
|
|
187
|
+
timestamp=msg.ts,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
self._msgbus.publish(topic="funding_rate", msg=funding_rate_msg)
|
|
191
|
+
self._msgbus.publish(topic="index_price", msg=index_price_msg)
|
|
192
|
+
self._msgbus.publish(topic="mark_price", msg=mark_price_msg)
|
|
193
|
+
|
|
194
|
+
def _handle_kline(self, raw: bytes):
|
|
195
|
+
msg: BybitWsKlineMsg = self._ws_msg_kline_decoder.decode(raw)
|
|
196
|
+
id = msg.topic.split(".")[-1] + self.market_type
|
|
197
|
+
symbol = self._market_id[id]
|
|
198
|
+
for d in msg.data:
|
|
199
|
+
interval = BybitEnumParser.parse_kline_interval(d.interval)
|
|
200
|
+
kline = Kline(
|
|
201
|
+
exchange=self._exchange_id,
|
|
202
|
+
symbol=symbol,
|
|
203
|
+
interval=interval,
|
|
204
|
+
open=float(d.open),
|
|
205
|
+
high=float(d.high),
|
|
206
|
+
low=float(d.low),
|
|
207
|
+
close=float(d.close),
|
|
208
|
+
volume=float(d.volume),
|
|
209
|
+
start=d.start,
|
|
210
|
+
confirm=d.confirm,
|
|
211
|
+
timestamp=msg.ts,
|
|
212
|
+
)
|
|
213
|
+
self._msgbus.publish(topic="kline", msg=kline)
|
|
214
|
+
|
|
215
|
+
def _handle_trade(self, raw: bytes):
|
|
216
|
+
msg: BybitWsTradeMsg = self._ws_msg_trade_decoder.decode(raw)
|
|
217
|
+
for d in msg.data:
|
|
218
|
+
id = d.s + self.market_type
|
|
219
|
+
symbol = self._market_id[id]
|
|
220
|
+
trade = Trade(
|
|
221
|
+
exchange=self._exchange_id,
|
|
222
|
+
symbol=symbol,
|
|
223
|
+
price=float(d.p),
|
|
224
|
+
size=float(d.v),
|
|
225
|
+
timestamp=msg.ts,
|
|
226
|
+
side=BybitEnumParser.parse_order_side(d.S),
|
|
227
|
+
)
|
|
228
|
+
self._msgbus.publish(topic="trade", msg=trade)
|
|
229
|
+
|
|
230
|
+
def _handle_orderbook(self, raw: bytes):
|
|
231
|
+
msg: BybitWsOrderbookDepthMsg = self._ws_msg_orderbook_decoder.decode(raw)
|
|
232
|
+
id = msg.data.s + self.market_type
|
|
233
|
+
symbol = self._market_id[id]
|
|
234
|
+
res = self._bookl1_orderbook[symbol].parse_orderbook_depth(msg, levels=1)
|
|
235
|
+
|
|
236
|
+
bid, bid_size = (
|
|
237
|
+
(res["bids"][0].price, res["bids"][0].size) if res["bids"] else (0, 0)
|
|
238
|
+
)
|
|
239
|
+
ask, ask_size = (
|
|
240
|
+
(res["asks"][0].price, res["asks"][0].size) if res["asks"] else (0, 0)
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
bookl1 = BookL1(
|
|
244
|
+
exchange=self._exchange_id,
|
|
245
|
+
symbol=symbol,
|
|
246
|
+
timestamp=msg.ts,
|
|
247
|
+
bid=bid,
|
|
248
|
+
bid_size=bid_size,
|
|
249
|
+
ask=ask,
|
|
250
|
+
ask_size=ask_size,
|
|
251
|
+
)
|
|
252
|
+
self._msgbus.publish(topic="bookl1", msg=bookl1)
|
|
253
|
+
|
|
254
|
+
def _handle_orderbook_depth(self, raw: bytes):
|
|
255
|
+
msg: BybitWsOrderbookDepthMsg = self._ws_msg_orderbook_decoder.decode(raw)
|
|
256
|
+
id = msg.data.s + self.market_type
|
|
257
|
+
symbol = self._market_id[id]
|
|
258
|
+
depth = int(msg.topic.split(".")[1])
|
|
259
|
+
res = self._bookl2_orderbook[symbol].parse_orderbook_depth(msg, levels=depth)
|
|
260
|
+
|
|
261
|
+
bids = res["bids"] if res["bids"] else [BookOrderData(price=0, size=0)]
|
|
262
|
+
asks = res["asks"] if res["asks"] else [BookOrderData(price=0, size=0)]
|
|
263
|
+
|
|
264
|
+
bookl2 = BookL2(
|
|
265
|
+
exchange=self._exchange_id,
|
|
266
|
+
symbol=symbol,
|
|
267
|
+
timestamp=msg.ts,
|
|
268
|
+
bids=bids,
|
|
269
|
+
asks=asks,
|
|
270
|
+
)
|
|
271
|
+
self._msgbus.publish(topic="bookl2", msg=bookl2)
|
|
272
|
+
|
|
273
|
+
async def request_ticker(
|
|
274
|
+
self,
|
|
275
|
+
symbol: str,
|
|
276
|
+
) -> Ticker:
|
|
277
|
+
"""Request 24hr ticker data"""
|
|
278
|
+
market = self._market.get(symbol)
|
|
279
|
+
if not market:
|
|
280
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
281
|
+
category = self._get_category(market)
|
|
282
|
+
id = market.id
|
|
283
|
+
ticker_response = await self._api_client.get_v5_market_tickers(
|
|
284
|
+
category=category, symbol=id
|
|
285
|
+
)
|
|
286
|
+
for ticker in ticker_response.result.list:
|
|
287
|
+
return Ticker(
|
|
288
|
+
exchange=self._exchange_id,
|
|
289
|
+
symbol=symbol,
|
|
290
|
+
last_price=float(ticker.lastPrice),
|
|
291
|
+
timestamp=ticker_response.time,
|
|
292
|
+
volume=float(ticker.volume24h),
|
|
293
|
+
volumeCcy=float(ticker.turnover24h),
|
|
294
|
+
)
|
|
295
|
+
raise ValueError(f"No ticker data found for symbol {symbol}")
|
|
296
|
+
|
|
297
|
+
async def request_all_tickers(
|
|
298
|
+
self,
|
|
299
|
+
) -> Dict[str, Ticker]:
|
|
300
|
+
"""Request 24hr ticker data for multiple symbols"""
|
|
301
|
+
if self._account_type.is_spot:
|
|
302
|
+
category = "spot"
|
|
303
|
+
elif self._account_type.is_linear:
|
|
304
|
+
category = "linear"
|
|
305
|
+
elif self._account_type.is_inverse:
|
|
306
|
+
category = "inverse"
|
|
307
|
+
ticker_response = await self._api_client.get_v5_market_tickers(
|
|
308
|
+
category=category,
|
|
309
|
+
)
|
|
310
|
+
tickers = {}
|
|
311
|
+
for ticker in ticker_response.result.list:
|
|
312
|
+
id = ticker.symbol + self.market_type
|
|
313
|
+
symbol = self._market_id.get(id)
|
|
314
|
+
if not symbol:
|
|
315
|
+
continue
|
|
316
|
+
tickers[symbol] = Ticker(
|
|
317
|
+
exchange=self._exchange_id,
|
|
318
|
+
symbol=symbol,
|
|
319
|
+
last_price=float(ticker.lastPrice),
|
|
320
|
+
timestamp=ticker_response.time,
|
|
321
|
+
volume=float(ticker.volume24h),
|
|
322
|
+
volumeCcy=float(ticker.turnover24h),
|
|
323
|
+
)
|
|
324
|
+
return tickers
|
|
325
|
+
|
|
326
|
+
async def request_index_klines(
|
|
327
|
+
self,
|
|
328
|
+
symbol: str,
|
|
329
|
+
interval: KlineInterval,
|
|
330
|
+
limit: int | None = None,
|
|
331
|
+
start_time: int | None = None,
|
|
332
|
+
end_time: int | None = None,
|
|
333
|
+
) -> KlineList:
|
|
334
|
+
market = self._market.get(symbol)
|
|
335
|
+
if not market:
|
|
336
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
337
|
+
if market.spot:
|
|
338
|
+
raise ValueError("Spot market is not supported for index klines")
|
|
339
|
+
category = self._get_category(market)
|
|
340
|
+
id = market.id
|
|
341
|
+
bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
|
|
342
|
+
all_klines: list[Kline] = []
|
|
343
|
+
seen_timestamps: set[int] = set()
|
|
344
|
+
prev_start_time: int | None = None
|
|
345
|
+
|
|
346
|
+
while True:
|
|
347
|
+
# Check for infinite loop condition
|
|
348
|
+
if prev_start_time is not None and prev_start_time == start_time:
|
|
349
|
+
break
|
|
350
|
+
prev_start_time = start_time
|
|
351
|
+
|
|
352
|
+
klines_response: BybitIndexKlineResponse = (
|
|
353
|
+
await self._api_client.get_v5_market_index_price_kline(
|
|
354
|
+
category=category,
|
|
355
|
+
symbol=id,
|
|
356
|
+
interval=bybit_interval.value,
|
|
357
|
+
limit=1000,
|
|
358
|
+
start=start_time,
|
|
359
|
+
end=end_time,
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
# Sort klines by start time and filter out duplicates
|
|
364
|
+
response_klines = sorted(
|
|
365
|
+
klines_response.result.list, key=lambda k: int(k.startTime)
|
|
366
|
+
)
|
|
367
|
+
klines: list[Kline] = [
|
|
368
|
+
self._handle_index_candlesticks(
|
|
369
|
+
symbol=symbol,
|
|
370
|
+
interval=interval,
|
|
371
|
+
kline=kline,
|
|
372
|
+
timestamp=klines_response.time,
|
|
373
|
+
)
|
|
374
|
+
for kline in response_klines
|
|
375
|
+
if int(kline.startTime) not in seen_timestamps
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
all_klines.extend(klines)
|
|
379
|
+
seen_timestamps.update(int(kline.startTime) for kline in response_klines)
|
|
380
|
+
|
|
381
|
+
# If no new klines were found, break
|
|
382
|
+
if not klines:
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
# Update the start_time to fetch the next set of bars
|
|
386
|
+
start_time = int(response_klines[-1].startTime) + 1
|
|
387
|
+
|
|
388
|
+
# No more bars to fetch if we've reached the end time
|
|
389
|
+
if end_time is not None and start_time >= end_time:
|
|
390
|
+
break
|
|
391
|
+
|
|
392
|
+
# If limit is specified, return the last 'limit' number of klines
|
|
393
|
+
if limit is not None and len(all_klines) > limit:
|
|
394
|
+
all_klines = all_klines[-limit:]
|
|
395
|
+
|
|
396
|
+
kline_list = KlineList(
|
|
397
|
+
all_klines,
|
|
398
|
+
fields=[
|
|
399
|
+
"timestamp",
|
|
400
|
+
"symbol",
|
|
401
|
+
"open",
|
|
402
|
+
"high",
|
|
403
|
+
"low",
|
|
404
|
+
"close",
|
|
405
|
+
"confirm",
|
|
406
|
+
],
|
|
407
|
+
)
|
|
408
|
+
return kline_list
|
|
409
|
+
|
|
410
|
+
async def request_klines(
|
|
411
|
+
self,
|
|
412
|
+
symbol: str,
|
|
413
|
+
interval: KlineInterval,
|
|
414
|
+
limit: int | None = None,
|
|
415
|
+
start_time: int | None = None,
|
|
416
|
+
end_time: int | None = None,
|
|
417
|
+
) -> KlineList:
|
|
418
|
+
market = self._market.get(symbol)
|
|
419
|
+
if not market:
|
|
420
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
421
|
+
category = self._get_category(market)
|
|
422
|
+
id = market.id
|
|
423
|
+
bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
|
|
424
|
+
all_klines: list[Kline] = []
|
|
425
|
+
seen_timestamps: set[int] = set()
|
|
426
|
+
prev_start_time: int | None = None
|
|
427
|
+
|
|
428
|
+
while True:
|
|
429
|
+
# Check for infinite loop condition
|
|
430
|
+
if prev_start_time is not None and prev_start_time == start_time:
|
|
431
|
+
break
|
|
432
|
+
prev_start_time = start_time
|
|
433
|
+
|
|
434
|
+
klines_response: BybitKlineResponse = (
|
|
435
|
+
await self._api_client.get_v5_market_kline(
|
|
436
|
+
category=category,
|
|
437
|
+
symbol=id,
|
|
438
|
+
interval=bybit_interval.value,
|
|
439
|
+
limit=1000,
|
|
440
|
+
start=start_time,
|
|
441
|
+
end=end_time,
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Sort klines by start time and filter out duplicates
|
|
446
|
+
response_klines = sorted(
|
|
447
|
+
klines_response.result.list, key=lambda k: int(k.startTime)
|
|
448
|
+
)
|
|
449
|
+
klines: list[Kline] = [
|
|
450
|
+
self._handle_candlesticks(
|
|
451
|
+
symbol=symbol,
|
|
452
|
+
interval=interval,
|
|
453
|
+
kline=kline,
|
|
454
|
+
timestamp=klines_response.time,
|
|
455
|
+
)
|
|
456
|
+
for kline in response_klines
|
|
457
|
+
if int(kline.startTime) not in seen_timestamps
|
|
458
|
+
]
|
|
459
|
+
|
|
460
|
+
all_klines.extend(klines)
|
|
461
|
+
seen_timestamps.update(int(kline.startTime) for kline in response_klines)
|
|
462
|
+
|
|
463
|
+
# If no new klines were found, break
|
|
464
|
+
if not klines:
|
|
465
|
+
break
|
|
466
|
+
|
|
467
|
+
# Update the start_time to fetch the next set of bars
|
|
468
|
+
start_time = int(response_klines[-1].startTime) + 1
|
|
469
|
+
|
|
470
|
+
# No more bars to fetch if we've reached the end time
|
|
471
|
+
if end_time is not None and start_time >= end_time:
|
|
472
|
+
break
|
|
473
|
+
|
|
474
|
+
# If limit is specified, return the last 'limit' number of klines
|
|
475
|
+
if limit is not None and len(all_klines) > limit:
|
|
476
|
+
all_klines = all_klines[-limit:]
|
|
477
|
+
|
|
478
|
+
kline_list = KlineList(
|
|
479
|
+
all_klines,
|
|
480
|
+
fields=[
|
|
481
|
+
"timestamp",
|
|
482
|
+
"symbol",
|
|
483
|
+
"open",
|
|
484
|
+
"high",
|
|
485
|
+
"low",
|
|
486
|
+
"close",
|
|
487
|
+
"volume",
|
|
488
|
+
"turnover",
|
|
489
|
+
"confirm",
|
|
490
|
+
],
|
|
491
|
+
)
|
|
492
|
+
return kline_list
|
|
493
|
+
|
|
494
|
+
def subscribe_funding_rate(self, symbol: str | List[str]):
|
|
495
|
+
symbols = []
|
|
496
|
+
if isinstance(symbol, str):
|
|
497
|
+
symbol = [symbol]
|
|
498
|
+
|
|
499
|
+
for s in symbol:
|
|
500
|
+
market = self._market.get(s)
|
|
501
|
+
if not market:
|
|
502
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
503
|
+
symbols.append(market.id)
|
|
504
|
+
|
|
505
|
+
self._ws_client.subscribe_ticker(symbols)
|
|
506
|
+
|
|
507
|
+
def unsubscribe_funding_rate(self, symbol: str | List[str]):
|
|
508
|
+
symbols = []
|
|
509
|
+
if isinstance(symbol, str):
|
|
510
|
+
symbol = [symbol]
|
|
511
|
+
|
|
512
|
+
for s in symbol:
|
|
513
|
+
market = self._market.get(s)
|
|
514
|
+
if not market:
|
|
515
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
516
|
+
symbols.append(market.id)
|
|
517
|
+
|
|
518
|
+
self._ws_client.unsubscribe_ticker(symbols)
|
|
519
|
+
|
|
520
|
+
def subscribe_index_price(self, symbol: str | List[str]):
|
|
521
|
+
symbols = []
|
|
522
|
+
if isinstance(symbol, str):
|
|
523
|
+
symbol = [symbol]
|
|
524
|
+
|
|
525
|
+
for s in symbol:
|
|
526
|
+
market = self._market.get(s)
|
|
527
|
+
if not market:
|
|
528
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
529
|
+
symbols.append(market.id)
|
|
530
|
+
|
|
531
|
+
self._ws_client.subscribe_ticker(symbols)
|
|
532
|
+
|
|
533
|
+
def unsubscribe_index_price(self, symbol: str | List[str]):
|
|
534
|
+
symbols = []
|
|
535
|
+
if isinstance(symbol, str):
|
|
536
|
+
symbol = [symbol]
|
|
537
|
+
|
|
538
|
+
for s in symbol:
|
|
539
|
+
market = self._market.get(s)
|
|
540
|
+
if not market:
|
|
541
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
542
|
+
symbols.append(market.id)
|
|
543
|
+
|
|
544
|
+
self._ws_client.unsubscribe_ticker(symbols)
|
|
545
|
+
|
|
546
|
+
def subscribe_mark_price(self, symbol: str | List[str]):
|
|
547
|
+
symbols = []
|
|
548
|
+
if isinstance(symbol, str):
|
|
549
|
+
symbol = [symbol]
|
|
550
|
+
|
|
551
|
+
for s in symbol:
|
|
552
|
+
market = self._market.get(s)
|
|
553
|
+
if not market:
|
|
554
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
555
|
+
symbols.append(market.id)
|
|
556
|
+
|
|
557
|
+
self._ws_client.subscribe_ticker(symbols)
|
|
558
|
+
|
|
559
|
+
def unsubscribe_mark_price(self, symbol: str | List[str]):
|
|
560
|
+
symbols = []
|
|
561
|
+
if isinstance(symbol, str):
|
|
562
|
+
symbol = [symbol]
|
|
563
|
+
|
|
564
|
+
for s in symbol:
|
|
565
|
+
market = self._market.get(s)
|
|
566
|
+
if not market:
|
|
567
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
568
|
+
symbols.append(market.id)
|
|
569
|
+
|
|
570
|
+
self._ws_client.unsubscribe_ticker(symbols)
|
|
571
|
+
|
|
572
|
+
def subscribe_bookl1(self, symbol: str | List[str]):
|
|
573
|
+
symbols = []
|
|
574
|
+
if isinstance(symbol, str):
|
|
575
|
+
symbol = [symbol]
|
|
576
|
+
|
|
577
|
+
for s in symbol:
|
|
578
|
+
market = self._market.get(s)
|
|
579
|
+
if not market:
|
|
580
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
581
|
+
symbols.append(market.id)
|
|
582
|
+
|
|
583
|
+
self._ws_client.subscribe_order_book(symbols, depth=1)
|
|
584
|
+
|
|
585
|
+
def unsubscribe_bookl1(self, symbol: str | List[str]):
|
|
586
|
+
symbols = []
|
|
587
|
+
if isinstance(symbol, str):
|
|
588
|
+
symbol = [symbol]
|
|
589
|
+
|
|
590
|
+
for s in symbol:
|
|
591
|
+
market = self._market.get(s)
|
|
592
|
+
if not market:
|
|
593
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
594
|
+
symbols.append(market.id)
|
|
595
|
+
|
|
596
|
+
self._ws_client.unsubscribe_order_book(symbols, depth=1)
|
|
597
|
+
|
|
598
|
+
def subscribe_trade(self, symbol: str | List[str]):
|
|
599
|
+
symbols = []
|
|
600
|
+
if isinstance(symbol, str):
|
|
601
|
+
symbol = [symbol]
|
|
602
|
+
|
|
603
|
+
for s in symbol:
|
|
604
|
+
market = self._market.get(s)
|
|
605
|
+
if not market:
|
|
606
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
607
|
+
symbols.append(market.id)
|
|
608
|
+
|
|
609
|
+
self._ws_client.subscribe_trade(symbols)
|
|
610
|
+
|
|
611
|
+
def unsubscribe_trade(self, symbol: str | List[str]):
|
|
612
|
+
symbols = []
|
|
613
|
+
if isinstance(symbol, str):
|
|
614
|
+
symbol = [symbol]
|
|
615
|
+
|
|
616
|
+
for s in symbol:
|
|
617
|
+
market = self._market.get(s)
|
|
618
|
+
if not market:
|
|
619
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
620
|
+
symbols.append(market.id)
|
|
621
|
+
|
|
622
|
+
self._ws_client.unsubscribe_trade(symbols)
|
|
623
|
+
|
|
624
|
+
def subscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
|
|
625
|
+
symbols = []
|
|
626
|
+
if isinstance(symbol, str):
|
|
627
|
+
symbol = [symbol]
|
|
628
|
+
|
|
629
|
+
for s in symbol:
|
|
630
|
+
market = self._market.get(s)
|
|
631
|
+
if not market:
|
|
632
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
633
|
+
symbols.append(market.id)
|
|
634
|
+
|
|
635
|
+
bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
|
|
636
|
+
self._ws_client.subscribe_kline(symbols, bybit_interval)
|
|
637
|
+
|
|
638
|
+
def unsubscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
|
|
639
|
+
symbols = []
|
|
640
|
+
if isinstance(symbol, str):
|
|
641
|
+
symbol = [symbol]
|
|
642
|
+
|
|
643
|
+
for s in symbol:
|
|
644
|
+
market = self._market.get(s)
|
|
645
|
+
if not market:
|
|
646
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
647
|
+
symbols.append(market.id)
|
|
648
|
+
|
|
649
|
+
bybit_interval = BybitEnumParser.to_bybit_kline_interval(interval)
|
|
650
|
+
self._ws_client.unsubscribe_kline(symbols, bybit_interval)
|
|
651
|
+
|
|
652
|
+
def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
|
|
653
|
+
supported = {BookLevel.L50, BookLevel.L200, BookLevel.L1000}
|
|
654
|
+
if level not in supported:
|
|
655
|
+
raise ValueError(
|
|
656
|
+
f"Unsupported book level for Bybit: {level}. Use L50, L200, or L1000."
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
symbols = []
|
|
660
|
+
if isinstance(symbol, str):
|
|
661
|
+
symbol = [symbol]
|
|
662
|
+
|
|
663
|
+
for s in symbol:
|
|
664
|
+
market = self._market.get(s)
|
|
665
|
+
if not market:
|
|
666
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
667
|
+
symbols.append(market.id)
|
|
668
|
+
|
|
669
|
+
self._ws_client.subscribe_order_book(symbols, depth=int(level.value))
|
|
670
|
+
|
|
671
|
+
def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
|
|
672
|
+
supported = {BookLevel.L50, BookLevel.L200, BookLevel.L1000}
|
|
673
|
+
if level not in supported:
|
|
674
|
+
raise ValueError(
|
|
675
|
+
f"Unsupported book level for Bybit: {level}. Use L50, L200, or L1000."
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
symbols = []
|
|
679
|
+
if isinstance(symbol, str):
|
|
680
|
+
symbol = [symbol]
|
|
681
|
+
|
|
682
|
+
for s in symbol:
|
|
683
|
+
market = self._market.get(s)
|
|
684
|
+
if not market:
|
|
685
|
+
raise ValueError(f"Symbol {s} formated wrongly, or not supported")
|
|
686
|
+
symbols.append(market.id)
|
|
687
|
+
|
|
688
|
+
self._ws_client.unsubscribe_order_book(symbols, depth=int(level.value))
|
|
689
|
+
|
|
690
|
+
def _handle_index_candlesticks(
|
|
691
|
+
self,
|
|
692
|
+
symbol: str,
|
|
693
|
+
interval: KlineInterval,
|
|
694
|
+
kline: BybitIndexKlineResponseArray,
|
|
695
|
+
timestamp: int,
|
|
696
|
+
) -> Kline:
|
|
697
|
+
local_timestamp = self._clock.timestamp_ms()
|
|
698
|
+
confirm = (
|
|
699
|
+
True
|
|
700
|
+
if local_timestamp >= int(kline.startTime) + interval.seconds * 1000 - 1
|
|
701
|
+
else False
|
|
702
|
+
)
|
|
703
|
+
return Kline(
|
|
704
|
+
exchange=self._exchange_id,
|
|
705
|
+
symbol=symbol,
|
|
706
|
+
interval=interval,
|
|
707
|
+
open=float(kline.openPrice),
|
|
708
|
+
high=float(kline.highPrice),
|
|
709
|
+
low=float(kline.lowPrice),
|
|
710
|
+
close=float(kline.closePrice),
|
|
711
|
+
start=int(kline.startTime),
|
|
712
|
+
timestamp=timestamp,
|
|
713
|
+
confirm=confirm,
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
def _handle_candlesticks(
|
|
717
|
+
self,
|
|
718
|
+
symbol: str,
|
|
719
|
+
interval: KlineInterval,
|
|
720
|
+
kline: BybitKlineResponseArray,
|
|
721
|
+
timestamp: int,
|
|
722
|
+
) -> Kline:
|
|
723
|
+
local_timestamp = self._clock.timestamp_ms()
|
|
724
|
+
confirm = (
|
|
725
|
+
True
|
|
726
|
+
if local_timestamp >= int(kline.startTime) + interval.seconds * 1000 - 1
|
|
727
|
+
else False
|
|
728
|
+
)
|
|
729
|
+
return Kline(
|
|
730
|
+
exchange=self._exchange_id,
|
|
731
|
+
symbol=symbol,
|
|
732
|
+
interval=interval,
|
|
733
|
+
open=float(kline.openPrice),
|
|
734
|
+
high=float(kline.highPrice),
|
|
735
|
+
low=float(kline.lowPrice),
|
|
736
|
+
close=float(kline.closePrice),
|
|
737
|
+
volume=float(kline.volume),
|
|
738
|
+
start=int(kline.startTime),
|
|
739
|
+
turnover=float(kline.turnover),
|
|
740
|
+
timestamp=timestamp,
|
|
741
|
+
confirm=confirm,
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
class BybitPrivateConnector(PrivateConnector):
|
|
746
|
+
_account_type: BybitAccountType
|
|
747
|
+
_market: Dict[str, BaseMarket]
|
|
748
|
+
_market_id: Dict[str, str]
|
|
749
|
+
_api_client: BybitApiClient
|
|
750
|
+
_oms: BybitOrderManagementSystem
|
|
751
|
+
|
|
752
|
+
def __init__(
|
|
753
|
+
self,
|
|
754
|
+
account_type: BybitAccountType,
|
|
755
|
+
exchange: BybitExchangeManager,
|
|
756
|
+
cache: AsyncCache,
|
|
757
|
+
registry: OrderRegistry,
|
|
758
|
+
clock: LiveClock,
|
|
759
|
+
msgbus: MessageBus,
|
|
760
|
+
task_manager: TaskManager,
|
|
761
|
+
order_query_config: OrderQueryConfig,
|
|
762
|
+
enable_rate_limit: bool = True,
|
|
763
|
+
max_subscriptions_per_client: int | None = None,
|
|
764
|
+
max_clients: int | None = None,
|
|
765
|
+
**kwargs,
|
|
766
|
+
):
|
|
767
|
+
if not exchange.api_key or not exchange.secret:
|
|
768
|
+
raise ValueError("API key and secret are required for private endpoints")
|
|
769
|
+
|
|
770
|
+
if account_type not in {
|
|
771
|
+
BybitAccountType.UNIFIED,
|
|
772
|
+
BybitAccountType.UNIFIED_TESTNET,
|
|
773
|
+
}:
|
|
774
|
+
raise ValueError(
|
|
775
|
+
"Please using `BybitAccountType.UNIFIED` or `BybitAccountType.UNIFIED_TESTNET` in `PrivateConnector`"
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
api_client = BybitApiClient(
|
|
779
|
+
clock=clock,
|
|
780
|
+
api_key=exchange.api_key,
|
|
781
|
+
secret=exchange.secret,
|
|
782
|
+
testnet=account_type.is_testnet,
|
|
783
|
+
enable_rate_limit=enable_rate_limit,
|
|
784
|
+
**kwargs,
|
|
785
|
+
)
|
|
786
|
+
|
|
787
|
+
oms = BybitOrderManagementSystem(
|
|
788
|
+
account_type=account_type,
|
|
789
|
+
api_key=exchange.api_key,
|
|
790
|
+
secret=exchange.secret,
|
|
791
|
+
market=cast(Dict[str, BaseMarket], exchange.market),
|
|
792
|
+
market_id=exchange.market_id,
|
|
793
|
+
registry=registry,
|
|
794
|
+
cache=cache,
|
|
795
|
+
api_client=api_client,
|
|
796
|
+
exchange_id=exchange.exchange_id,
|
|
797
|
+
clock=clock,
|
|
798
|
+
msgbus=msgbus,
|
|
799
|
+
task_manager=task_manager,
|
|
800
|
+
enable_rate_limit=enable_rate_limit,
|
|
801
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
802
|
+
max_clients=max_clients,
|
|
803
|
+
order_query_config=order_query_config,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
super().__init__(
|
|
807
|
+
account_type=account_type,
|
|
808
|
+
market=cast(Dict[str, BaseMarket], exchange.market),
|
|
809
|
+
api_client=api_client,
|
|
810
|
+
task_manager=task_manager,
|
|
811
|
+
oms=oms,
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
async def connect(self):
|
|
815
|
+
self._oms._ws_client.subscribe_order()
|
|
816
|
+
self._oms._ws_client.subscribe_position()
|
|
817
|
+
self._oms._ws_client.subscribe_wallet()
|
|
818
|
+
await self._oms._ws_client.connect()
|
|
819
|
+
await self._oms._ws_api_client.connect()
|