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,931 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
from typing import Dict, List # noqa: F401
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from walrasquant.config import OrderQueryConfig
|
|
6
|
+
from walrasquant.exchange.okx import OkxAccountType
|
|
7
|
+
from walrasquant.exchange.okx.websockets import OkxWSClient
|
|
8
|
+
from walrasquant.exchange.okx.exchange import OkxExchangeManager
|
|
9
|
+
from walrasquant.exchange.okx.schema import OkxWsGeneralMsg
|
|
10
|
+
from walrasquant.exchange.okx.oms import OkxOrderManagementSystem
|
|
11
|
+
from walrasquant.schema import (
|
|
12
|
+
Trade,
|
|
13
|
+
BookL1,
|
|
14
|
+
Kline,
|
|
15
|
+
BookL2,
|
|
16
|
+
IndexPrice,
|
|
17
|
+
FundingRate,
|
|
18
|
+
MarkPrice,
|
|
19
|
+
KlineList,
|
|
20
|
+
Ticker,
|
|
21
|
+
)
|
|
22
|
+
from walrasquant.exchange.okx.schema import (
|
|
23
|
+
OkxMarket,
|
|
24
|
+
OkxWsBboTbtMsg,
|
|
25
|
+
OkxWsCandleMsg,
|
|
26
|
+
OkxWsTradeMsg,
|
|
27
|
+
OkxWsIndexTickerMsg,
|
|
28
|
+
OkxWsFundingRateMsg,
|
|
29
|
+
OkxWsMarkPriceMsg,
|
|
30
|
+
OkxCandlesticksResponse,
|
|
31
|
+
OkxCandlesticksResponseData,
|
|
32
|
+
OkxWsBook5Msg,
|
|
33
|
+
OkxTickersResponse,
|
|
34
|
+
OkxIndexCandlesticksResponseData,
|
|
35
|
+
OkxWsBooksMsg,
|
|
36
|
+
OkxOrderBook,
|
|
37
|
+
)
|
|
38
|
+
from walrasquant.constants import (
|
|
39
|
+
KlineInterval,
|
|
40
|
+
BookLevel,
|
|
41
|
+
)
|
|
42
|
+
from walrasquant.base import PublicConnector, PrivateConnector
|
|
43
|
+
from walrasquant.core.nautilius_core import MessageBus, LiveClock
|
|
44
|
+
from walrasquant.core.cache import AsyncCache
|
|
45
|
+
from walrasquant.core.entity import TaskManager
|
|
46
|
+
from walrasquant.core.registry import OrderRegistry
|
|
47
|
+
from walrasquant.exchange.okx.rest_api import OkxApiClient
|
|
48
|
+
from walrasquant.exchange.okx.constants import (
|
|
49
|
+
OkxEnumParser,
|
|
50
|
+
OkxKlineInterval,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class OkxPublicConnector(PublicConnector):
|
|
55
|
+
_ws_client: OkxWSClient
|
|
56
|
+
_api_client: OkxApiClient
|
|
57
|
+
_account_type: OkxAccountType
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
account_type: OkxAccountType,
|
|
62
|
+
exchange: OkxExchangeManager,
|
|
63
|
+
msgbus: MessageBus,
|
|
64
|
+
clock: LiveClock,
|
|
65
|
+
task_manager: TaskManager,
|
|
66
|
+
custom_url: str | None = None,
|
|
67
|
+
enable_rate_limit: bool = True,
|
|
68
|
+
max_subscriptions_per_client: int | None = None,
|
|
69
|
+
max_clients: int | None = None,
|
|
70
|
+
):
|
|
71
|
+
super().__init__(
|
|
72
|
+
account_type=account_type,
|
|
73
|
+
market=exchange.market,
|
|
74
|
+
market_id=exchange.market_id,
|
|
75
|
+
exchange_id=exchange.exchange_id,
|
|
76
|
+
ws_client=OkxWSClient(
|
|
77
|
+
account_type=account_type,
|
|
78
|
+
handler=self._ws_msg_handler,
|
|
79
|
+
task_manager=task_manager,
|
|
80
|
+
custom_url=custom_url,
|
|
81
|
+
clock=clock,
|
|
82
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
83
|
+
max_clients=max_clients,
|
|
84
|
+
),
|
|
85
|
+
msgbus=msgbus,
|
|
86
|
+
clock=clock,
|
|
87
|
+
api_client=OkxApiClient(
|
|
88
|
+
clock=clock,
|
|
89
|
+
testnet=account_type.is_testnet,
|
|
90
|
+
enable_rate_limit=enable_rate_limit,
|
|
91
|
+
),
|
|
92
|
+
task_manager=task_manager,
|
|
93
|
+
)
|
|
94
|
+
self._business_ws_client = OkxWSClient(
|
|
95
|
+
account_type=account_type,
|
|
96
|
+
handler=self._business_ws_msg_handler,
|
|
97
|
+
task_manager=task_manager,
|
|
98
|
+
business_url=True,
|
|
99
|
+
custom_url=custom_url,
|
|
100
|
+
clock=clock,
|
|
101
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
102
|
+
max_clients=max_clients,
|
|
103
|
+
)
|
|
104
|
+
self._ws_msg_general_decoder = msgspec.json.Decoder(OkxWsGeneralMsg)
|
|
105
|
+
self._ws_msg_bbo_tbt_decoder = msgspec.json.Decoder(OkxWsBboTbtMsg)
|
|
106
|
+
self._ws_msg_book5_decoder = msgspec.json.Decoder(OkxWsBook5Msg)
|
|
107
|
+
self._ws_msg_books_decoder = msgspec.json.Decoder(OkxWsBooksMsg)
|
|
108
|
+
self._ws_msg_candle_decoder = msgspec.json.Decoder(OkxWsCandleMsg)
|
|
109
|
+
self._ws_msg_trade_decoder = msgspec.json.Decoder(OkxWsTradeMsg)
|
|
110
|
+
self._ws_msg_index_ticker_decoder = msgspec.json.Decoder(OkxWsIndexTickerMsg)
|
|
111
|
+
self._ws_msg_mark_price_decoder = msgspec.json.Decoder(OkxWsMarkPriceMsg)
|
|
112
|
+
self._ws_msg_funding_rate_decoder = msgspec.json.Decoder(OkxWsFundingRateMsg)
|
|
113
|
+
self._okx_orderbook: Dict[str, OkxOrderBook] = {}
|
|
114
|
+
|
|
115
|
+
def _require_channel(self, ws_msg: OkxWsGeneralMsg) -> str:
|
|
116
|
+
arg = ws_msg.arg
|
|
117
|
+
channel = arg.channel if arg is not None else None
|
|
118
|
+
if channel is None:
|
|
119
|
+
raise ValueError("Missing channel in OKX websocket message")
|
|
120
|
+
return channel
|
|
121
|
+
|
|
122
|
+
def _require_arg_channel(self, channel: str | None) -> str:
|
|
123
|
+
if channel is None:
|
|
124
|
+
raise ValueError("Missing channel in OKX websocket message")
|
|
125
|
+
return channel
|
|
126
|
+
|
|
127
|
+
def _require_inst_id(self, inst_id: str | None) -> str:
|
|
128
|
+
if inst_id is None:
|
|
129
|
+
raise ValueError("Missing instrument id in OKX websocket message")
|
|
130
|
+
return inst_id
|
|
131
|
+
|
|
132
|
+
async def request_ticker(
|
|
133
|
+
self,
|
|
134
|
+
symbol: str,
|
|
135
|
+
) -> Ticker:
|
|
136
|
+
"""Request 24hr ticker data"""
|
|
137
|
+
market = self._market.get(symbol)
|
|
138
|
+
if not market:
|
|
139
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
140
|
+
|
|
141
|
+
ticker_response: OkxTickersResponse = (
|
|
142
|
+
await self._api_client.get_api_v5_market_ticker(inst_id=market.id)
|
|
143
|
+
)
|
|
144
|
+
for item in ticker_response.data:
|
|
145
|
+
ticker = Ticker(
|
|
146
|
+
exchange=self._exchange_id,
|
|
147
|
+
symbol=symbol,
|
|
148
|
+
last_price=float(item.last) if item.last else 0.0,
|
|
149
|
+
timestamp=int(item.ts),
|
|
150
|
+
volume=float(item.vol24h) if item.vol24h else 0.0,
|
|
151
|
+
volumeCcy=float(item.volCcy24h) if item.volCcy24h else 0.0,
|
|
152
|
+
)
|
|
153
|
+
return ticker
|
|
154
|
+
raise RuntimeError(f"Ticker data for symbol {symbol} not found")
|
|
155
|
+
|
|
156
|
+
async def request_all_tickers(
|
|
157
|
+
self,
|
|
158
|
+
) -> Dict[str, Ticker]:
|
|
159
|
+
"""Request 24hr ticker data for multiple symbols"""
|
|
160
|
+
spot_tickers_response: OkxTickersResponse = (
|
|
161
|
+
await self._api_client.get_api_v5_market_tickers(inst_type="SPOT")
|
|
162
|
+
)
|
|
163
|
+
swap_tickers_response: OkxTickersResponse = (
|
|
164
|
+
await self._api_client.get_api_v5_market_tickers(inst_type="SWAP")
|
|
165
|
+
)
|
|
166
|
+
future_tickers_response: OkxTickersResponse = (
|
|
167
|
+
await self._api_client.get_api_v5_market_tickers(inst_type="FUTURES")
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
tickers = {}
|
|
171
|
+
for item in (
|
|
172
|
+
spot_tickers_response.data
|
|
173
|
+
+ swap_tickers_response.data
|
|
174
|
+
+ future_tickers_response.data
|
|
175
|
+
):
|
|
176
|
+
symbol = self._market_id.get(item.instId)
|
|
177
|
+
if not symbol:
|
|
178
|
+
continue
|
|
179
|
+
tickers[symbol] = Ticker(
|
|
180
|
+
exchange=self._exchange_id,
|
|
181
|
+
symbol=symbol,
|
|
182
|
+
last_price=float(item.last) if item.last else 0.0,
|
|
183
|
+
timestamp=int(item.ts),
|
|
184
|
+
volume=float(item.vol24h) if item.vol24h else 0.0,
|
|
185
|
+
volumeCcy=float(item.volCcy24h) if item.volCcy24h else 0.0,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return tickers
|
|
189
|
+
|
|
190
|
+
async def request_index_klines(
|
|
191
|
+
self,
|
|
192
|
+
symbol: str,
|
|
193
|
+
interval: KlineInterval,
|
|
194
|
+
limit: int | None = None,
|
|
195
|
+
start_time: int | None = None,
|
|
196
|
+
end_time: int | None = None,
|
|
197
|
+
) -> KlineList:
|
|
198
|
+
market = self._market.get(symbol)
|
|
199
|
+
if not market:
|
|
200
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
201
|
+
|
|
202
|
+
okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
|
|
203
|
+
all_klines: list[Kline] = []
|
|
204
|
+
seen_timestamps: set[int] = set()
|
|
205
|
+
|
|
206
|
+
# First request to get the most recent data using before parameter
|
|
207
|
+
klines_response = (
|
|
208
|
+
await self._api_client.get_api_v5_market_history_index_candles(
|
|
209
|
+
instId=self._market[symbol].id,
|
|
210
|
+
bar=okx_interval.value,
|
|
211
|
+
limit=100, # Maximum allowed by the API is 100
|
|
212
|
+
before="0", # Get the latest klines
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
|
|
217
|
+
klines = [
|
|
218
|
+
self._handle_index_candlesticks(
|
|
219
|
+
symbol=symbol, interval=interval, kline=kline
|
|
220
|
+
)
|
|
221
|
+
for kline in response_klines
|
|
222
|
+
if int(kline.ts) not in seen_timestamps
|
|
223
|
+
]
|
|
224
|
+
all_klines.extend(klines)
|
|
225
|
+
seen_timestamps.update(int(kline.ts) for kline in response_klines)
|
|
226
|
+
|
|
227
|
+
# Continue fetching older data using after parameter if needed
|
|
228
|
+
if (
|
|
229
|
+
start_time is not None
|
|
230
|
+
and all_klines
|
|
231
|
+
and int(all_klines[0].timestamp) > start_time
|
|
232
|
+
):
|
|
233
|
+
while True:
|
|
234
|
+
# Use the oldest timestamp we have as the 'after' parameter
|
|
235
|
+
oldest_timestamp = (
|
|
236
|
+
min(int(kline.ts) for kline in response_klines)
|
|
237
|
+
if response_klines
|
|
238
|
+
else None
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not oldest_timestamp or (
|
|
242
|
+
start_time is not None and oldest_timestamp <= start_time
|
|
243
|
+
):
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
klines_response = (
|
|
247
|
+
await self._api_client.get_api_v5_market_history_index_candles(
|
|
248
|
+
instId=self._market[symbol].id,
|
|
249
|
+
bar=okx_interval.value,
|
|
250
|
+
limit=100,
|
|
251
|
+
after=str(oldest_timestamp), # Get klines before this timestamp
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
|
|
256
|
+
if not response_klines:
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
# Process klines and filter out duplicates
|
|
260
|
+
new_klines = [
|
|
261
|
+
self._handle_index_candlesticks(
|
|
262
|
+
symbol=symbol, interval=interval, kline=kline
|
|
263
|
+
)
|
|
264
|
+
for kline in response_klines
|
|
265
|
+
if int(kline.ts) not in seen_timestamps
|
|
266
|
+
]
|
|
267
|
+
|
|
268
|
+
if not new_klines:
|
|
269
|
+
break
|
|
270
|
+
|
|
271
|
+
all_klines = (
|
|
272
|
+
new_klines + all_klines
|
|
273
|
+
) # Prepend new klines as they are older
|
|
274
|
+
seen_timestamps.update(int(kline.ts) for kline in response_klines)
|
|
275
|
+
|
|
276
|
+
# Apply limit if specified
|
|
277
|
+
if limit is not None and len(all_klines) > limit:
|
|
278
|
+
all_klines = all_klines[-limit:] # Take the most recent klines
|
|
279
|
+
|
|
280
|
+
if end_time:
|
|
281
|
+
all_klines = [kline for kline in all_klines if kline.timestamp < end_time]
|
|
282
|
+
|
|
283
|
+
kline_list = KlineList(
|
|
284
|
+
all_klines,
|
|
285
|
+
fields=[
|
|
286
|
+
"timestamp",
|
|
287
|
+
"symbol",
|
|
288
|
+
"open",
|
|
289
|
+
"high",
|
|
290
|
+
"low",
|
|
291
|
+
"close",
|
|
292
|
+
"confirm",
|
|
293
|
+
],
|
|
294
|
+
)
|
|
295
|
+
return kline_list
|
|
296
|
+
|
|
297
|
+
async def request_klines(
|
|
298
|
+
self,
|
|
299
|
+
symbol: str,
|
|
300
|
+
interval: KlineInterval,
|
|
301
|
+
limit: int | None = None,
|
|
302
|
+
start_time: int | None = None,
|
|
303
|
+
end_time: int | None = None,
|
|
304
|
+
) -> KlineList:
|
|
305
|
+
market = self._market.get(symbol)
|
|
306
|
+
if not market:
|
|
307
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
308
|
+
|
|
309
|
+
okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
|
|
310
|
+
all_klines: list[Kline] = []
|
|
311
|
+
seen_timestamps: set[int] = set()
|
|
312
|
+
|
|
313
|
+
# First request to get the most recent data using before parameter
|
|
314
|
+
klines_response: OkxCandlesticksResponse = (
|
|
315
|
+
await self._api_client.get_api_v5_market_candles(
|
|
316
|
+
instId=self._market[symbol].id,
|
|
317
|
+
bar=okx_interval.value,
|
|
318
|
+
limit=100, # Maximum allowed by the API is 100
|
|
319
|
+
before="0", # Get the latest klines
|
|
320
|
+
)
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
|
|
324
|
+
klines = [
|
|
325
|
+
self._handle_candlesticks(symbol=symbol, interval=interval, kline=kline)
|
|
326
|
+
for kline in response_klines
|
|
327
|
+
if int(kline.ts) not in seen_timestamps
|
|
328
|
+
]
|
|
329
|
+
all_klines.extend(klines)
|
|
330
|
+
seen_timestamps.update(int(kline.ts) for kline in response_klines)
|
|
331
|
+
|
|
332
|
+
# Continue fetching older data using after parameter if needed
|
|
333
|
+
if (
|
|
334
|
+
start_time is not None
|
|
335
|
+
and all_klines
|
|
336
|
+
and int(all_klines[0].timestamp) > start_time
|
|
337
|
+
):
|
|
338
|
+
while True:
|
|
339
|
+
# Use the oldest timestamp we have as the 'after' parameter
|
|
340
|
+
oldest_timestamp = (
|
|
341
|
+
min(int(kline.ts) for kline in response_klines)
|
|
342
|
+
if response_klines
|
|
343
|
+
else None
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
if not oldest_timestamp or (
|
|
347
|
+
start_time is not None and oldest_timestamp <= start_time
|
|
348
|
+
):
|
|
349
|
+
break
|
|
350
|
+
|
|
351
|
+
klines_response = (
|
|
352
|
+
await self._api_client.get_api_v5_market_history_candles(
|
|
353
|
+
instId=self._market[symbol].id,
|
|
354
|
+
bar=okx_interval.value,
|
|
355
|
+
limit="100",
|
|
356
|
+
after=str(oldest_timestamp), # Get klines before this timestamp
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
response_klines = sorted(klines_response.data, key=lambda x: int(x.ts))
|
|
361
|
+
if not response_klines:
|
|
362
|
+
break
|
|
363
|
+
|
|
364
|
+
# Process klines and filter out duplicates
|
|
365
|
+
new_klines = [
|
|
366
|
+
self._handle_candlesticks(
|
|
367
|
+
symbol=symbol, interval=interval, kline=kline
|
|
368
|
+
)
|
|
369
|
+
for kline in response_klines
|
|
370
|
+
if int(kline.ts) not in seen_timestamps
|
|
371
|
+
]
|
|
372
|
+
|
|
373
|
+
if not new_klines:
|
|
374
|
+
break
|
|
375
|
+
|
|
376
|
+
all_klines = (
|
|
377
|
+
new_klines + all_klines
|
|
378
|
+
) # Prepend new klines as they are older
|
|
379
|
+
seen_timestamps.update(int(kline.ts) for kline in response_klines)
|
|
380
|
+
|
|
381
|
+
# Apply limit if specified
|
|
382
|
+
if limit is not None and len(all_klines) > limit:
|
|
383
|
+
all_klines = all_klines[-limit:] # Take the most recent klines
|
|
384
|
+
|
|
385
|
+
if end_time:
|
|
386
|
+
all_klines = [kline for kline in all_klines if kline.timestamp < end_time]
|
|
387
|
+
|
|
388
|
+
kline_list = KlineList(
|
|
389
|
+
all_klines,
|
|
390
|
+
fields=[
|
|
391
|
+
"timestamp",
|
|
392
|
+
"symbol",
|
|
393
|
+
"open",
|
|
394
|
+
"high",
|
|
395
|
+
"low",
|
|
396
|
+
"close",
|
|
397
|
+
"volume",
|
|
398
|
+
"quote_volume",
|
|
399
|
+
"confirm",
|
|
400
|
+
],
|
|
401
|
+
)
|
|
402
|
+
return kline_list
|
|
403
|
+
|
|
404
|
+
def subscribe_trade(self, symbol: str | List[str]):
|
|
405
|
+
symbols = []
|
|
406
|
+
if isinstance(symbol, str):
|
|
407
|
+
symbol = [symbol]
|
|
408
|
+
|
|
409
|
+
for s in symbol:
|
|
410
|
+
market = self._market.get(s)
|
|
411
|
+
if not market:
|
|
412
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
413
|
+
symbols.append(market.id)
|
|
414
|
+
|
|
415
|
+
self._ws_client.subscribe_trade(symbols)
|
|
416
|
+
|
|
417
|
+
def subscribe_bookl1(self, symbol: str | List[str]):
|
|
418
|
+
symbols = []
|
|
419
|
+
if isinstance(symbol, str):
|
|
420
|
+
symbol = [symbol]
|
|
421
|
+
|
|
422
|
+
for s in symbol:
|
|
423
|
+
market = self._market.get(s)
|
|
424
|
+
if not market:
|
|
425
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
426
|
+
symbols.append(market.id)
|
|
427
|
+
|
|
428
|
+
self._ws_client.subscribe_order_book(symbols, channel="bbo-tbt")
|
|
429
|
+
|
|
430
|
+
def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
|
|
431
|
+
if isinstance(symbol, str):
|
|
432
|
+
symbol = [symbol]
|
|
433
|
+
|
|
434
|
+
if level == BookLevel.L5:
|
|
435
|
+
symbols = []
|
|
436
|
+
for s in symbol:
|
|
437
|
+
market = self._market.get(s)
|
|
438
|
+
if not market:
|
|
439
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
440
|
+
symbols.append(market.id)
|
|
441
|
+
self._ws_client.subscribe_order_book(symbols, channel="books5")
|
|
442
|
+
elif level == BookLevel.L400:
|
|
443
|
+
symbols = []
|
|
444
|
+
for s in symbol:
|
|
445
|
+
market = self._market.get(s)
|
|
446
|
+
if not market:
|
|
447
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
448
|
+
symbols.append(market.id)
|
|
449
|
+
self._okx_orderbook[s] = OkxOrderBook()
|
|
450
|
+
self._ws_client.subscribe_order_book(symbols, channel="books")
|
|
451
|
+
else:
|
|
452
|
+
raise ValueError(
|
|
453
|
+
f"OKX only supports BookLevel.L5 or BookLevel.L400, got {level}"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def subscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
|
|
457
|
+
symbols = []
|
|
458
|
+
if isinstance(symbol, str):
|
|
459
|
+
symbol = [symbol]
|
|
460
|
+
|
|
461
|
+
for s in symbol:
|
|
462
|
+
market = self._market.get(s)
|
|
463
|
+
if not market:
|
|
464
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
465
|
+
symbols.append(market.id)
|
|
466
|
+
|
|
467
|
+
okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
|
|
468
|
+
self._business_ws_client.subscribe_candlesticks(symbols, okx_interval)
|
|
469
|
+
|
|
470
|
+
def subscribe_funding_rate(self, symbol: str | List[str]):
|
|
471
|
+
symbols = []
|
|
472
|
+
if isinstance(symbol, str):
|
|
473
|
+
symbol = [symbol]
|
|
474
|
+
|
|
475
|
+
for s in symbol:
|
|
476
|
+
market = self._market.get(s)
|
|
477
|
+
if not market:
|
|
478
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
479
|
+
symbols.append(market.id)
|
|
480
|
+
|
|
481
|
+
self._ws_client.subscribe_funding_rate(symbols)
|
|
482
|
+
|
|
483
|
+
def subscribe_index_price(self, symbol: str | List[str]):
|
|
484
|
+
symbols = []
|
|
485
|
+
if isinstance(symbol, str):
|
|
486
|
+
symbol = [symbol]
|
|
487
|
+
|
|
488
|
+
for s in symbol:
|
|
489
|
+
market = self._market.get(s)
|
|
490
|
+
if not market:
|
|
491
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
492
|
+
symbols.append(market.id)
|
|
493
|
+
|
|
494
|
+
self._ws_client.subscribe_index_price(symbols)
|
|
495
|
+
|
|
496
|
+
def subscribe_mark_price(self, symbol: str | List[str]):
|
|
497
|
+
symbols = []
|
|
498
|
+
if isinstance(symbol, str):
|
|
499
|
+
symbol = [symbol]
|
|
500
|
+
|
|
501
|
+
for s in symbol:
|
|
502
|
+
market = self._market.get(s)
|
|
503
|
+
if not market:
|
|
504
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
505
|
+
symbols.append(market.id)
|
|
506
|
+
|
|
507
|
+
self._ws_client.subscribe_mark_price(symbols)
|
|
508
|
+
|
|
509
|
+
def unsubscribe_trade(self, symbol: str | List[str]):
|
|
510
|
+
symbols = []
|
|
511
|
+
if isinstance(symbol, str):
|
|
512
|
+
symbol = [symbol]
|
|
513
|
+
|
|
514
|
+
for s in symbol:
|
|
515
|
+
market = self._market.get(s)
|
|
516
|
+
if not market:
|
|
517
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
518
|
+
symbols.append(market.id)
|
|
519
|
+
|
|
520
|
+
self._ws_client.unsubscribe_trade(symbols)
|
|
521
|
+
|
|
522
|
+
def unsubscribe_bookl1(self, symbol: str | List[str]):
|
|
523
|
+
symbols = []
|
|
524
|
+
if isinstance(symbol, str):
|
|
525
|
+
symbol = [symbol]
|
|
526
|
+
|
|
527
|
+
for s in symbol:
|
|
528
|
+
market = self._market.get(s)
|
|
529
|
+
if not market:
|
|
530
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
531
|
+
symbols.append(market.id)
|
|
532
|
+
|
|
533
|
+
self._ws_client.unsubscribe_order_book(symbols, channel="bbo-tbt")
|
|
534
|
+
|
|
535
|
+
def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
|
|
536
|
+
if isinstance(symbol, str):
|
|
537
|
+
symbol = [symbol]
|
|
538
|
+
|
|
539
|
+
if level == BookLevel.L5:
|
|
540
|
+
symbols = []
|
|
541
|
+
for s in symbol:
|
|
542
|
+
market = self._market.get(s)
|
|
543
|
+
if not market:
|
|
544
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
545
|
+
symbols.append(market.id)
|
|
546
|
+
self._ws_client.unsubscribe_order_book(symbols, channel="books5")
|
|
547
|
+
elif level == BookLevel.L400:
|
|
548
|
+
symbols = []
|
|
549
|
+
for s in symbol:
|
|
550
|
+
market = self._market.get(s)
|
|
551
|
+
if not market:
|
|
552
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
553
|
+
symbols.append(market.id)
|
|
554
|
+
self._okx_orderbook.pop(s, None)
|
|
555
|
+
self._ws_client.unsubscribe_order_book(symbols, channel="books")
|
|
556
|
+
else:
|
|
557
|
+
raise ValueError(
|
|
558
|
+
f"OKX only supports BookLevel.L5 or BookLevel.L400, got {level}"
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
def unsubscribe_kline(self, symbol: str | List[str], interval: KlineInterval):
|
|
562
|
+
symbols = []
|
|
563
|
+
if isinstance(symbol, str):
|
|
564
|
+
symbol = [symbol]
|
|
565
|
+
|
|
566
|
+
for s in symbol:
|
|
567
|
+
market = self._market.get(s)
|
|
568
|
+
if not market:
|
|
569
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
570
|
+
symbols.append(market.id)
|
|
571
|
+
|
|
572
|
+
okx_interval = OkxEnumParser.to_okx_kline_interval(interval)
|
|
573
|
+
self._business_ws_client.unsubscribe_candlesticks(symbols, okx_interval)
|
|
574
|
+
|
|
575
|
+
def unsubscribe_funding_rate(self, symbol: str | List[str]):
|
|
576
|
+
symbols = []
|
|
577
|
+
if isinstance(symbol, str):
|
|
578
|
+
symbol = [symbol]
|
|
579
|
+
|
|
580
|
+
for s in symbol:
|
|
581
|
+
market = self._market.get(s)
|
|
582
|
+
if not market:
|
|
583
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
584
|
+
symbols.append(market.id)
|
|
585
|
+
|
|
586
|
+
self._ws_client.unsubscribe_funding_rate(symbols)
|
|
587
|
+
|
|
588
|
+
def unsubscribe_index_price(self, symbol: str | List[str]):
|
|
589
|
+
symbols = []
|
|
590
|
+
if isinstance(symbol, str):
|
|
591
|
+
symbol = [symbol]
|
|
592
|
+
|
|
593
|
+
for s in symbol:
|
|
594
|
+
market = self._market.get(s)
|
|
595
|
+
if not market:
|
|
596
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
597
|
+
symbols.append(market.id)
|
|
598
|
+
|
|
599
|
+
self._ws_client.unsubscribe_index_price(symbols)
|
|
600
|
+
|
|
601
|
+
def unsubscribe_mark_price(self, symbol: str | List[str]):
|
|
602
|
+
symbols = []
|
|
603
|
+
if isinstance(symbol, str):
|
|
604
|
+
symbol = [symbol]
|
|
605
|
+
|
|
606
|
+
for s in symbol:
|
|
607
|
+
market = self._market.get(s)
|
|
608
|
+
if not market:
|
|
609
|
+
raise ValueError(f"Symbol {s} not found in market")
|
|
610
|
+
symbols.append(market.id)
|
|
611
|
+
|
|
612
|
+
self._ws_client.unsubscribe_mark_price(symbols)
|
|
613
|
+
|
|
614
|
+
def _business_ws_msg_handler(self, raw: bytes):
|
|
615
|
+
# if raw == b"pong":
|
|
616
|
+
# self._business_ws_client._transport.notify_user_specific_pong_received()
|
|
617
|
+
# self._log.debug(f"Pong received:{str(raw)}")
|
|
618
|
+
# return
|
|
619
|
+
try:
|
|
620
|
+
ws_msg: OkxWsGeneralMsg = self._ws_msg_general_decoder.decode(raw)
|
|
621
|
+
if ws_msg.is_event_msg:
|
|
622
|
+
self._handle_event_msg(ws_msg)
|
|
623
|
+
else:
|
|
624
|
+
channel = self._require_channel(ws_msg)
|
|
625
|
+
if channel.startswith("candle"):
|
|
626
|
+
self._handle_kline(raw)
|
|
627
|
+
except msgspec.DecodeError:
|
|
628
|
+
self._log.error(f"Error decoding message: {str(raw)}")
|
|
629
|
+
|
|
630
|
+
def _ws_msg_handler(self, raw: bytes):
|
|
631
|
+
# if raw == b"pong":
|
|
632
|
+
# self._ws_client._transport.notify_user_specific_pong_received()
|
|
633
|
+
# self._log.debug(f"Pong received:{raw.decode()}")
|
|
634
|
+
# return
|
|
635
|
+
try:
|
|
636
|
+
ws_msg: OkxWsGeneralMsg = self._ws_msg_general_decoder.decode(raw)
|
|
637
|
+
if ws_msg.is_event_msg:
|
|
638
|
+
self._handle_event_msg(ws_msg)
|
|
639
|
+
else:
|
|
640
|
+
channel = self._require_channel(ws_msg)
|
|
641
|
+
if channel == "bbo-tbt":
|
|
642
|
+
self._handle_bbo_tbt(raw)
|
|
643
|
+
elif channel == "trades":
|
|
644
|
+
self._handle_trade(raw)
|
|
645
|
+
elif channel.startswith("candle"):
|
|
646
|
+
self._handle_kline(raw)
|
|
647
|
+
elif channel == "books5":
|
|
648
|
+
self._handle_book5(raw)
|
|
649
|
+
elif channel == "books":
|
|
650
|
+
self._handle_books(raw)
|
|
651
|
+
elif channel == "index-ticker":
|
|
652
|
+
self._handle_index_ticker(raw)
|
|
653
|
+
elif channel == "mark-price":
|
|
654
|
+
self._handle_mark_price(raw)
|
|
655
|
+
elif channel == "funding-rate":
|
|
656
|
+
self._handle_funding_rate(raw)
|
|
657
|
+
except msgspec.DecodeError as e:
|
|
658
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
659
|
+
|
|
660
|
+
def _handle_index_ticker(self, raw: bytes):
|
|
661
|
+
msg: OkxWsIndexTickerMsg = self._ws_msg_index_ticker_decoder.decode(raw)
|
|
662
|
+
|
|
663
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
664
|
+
symbol = self._market_id[id]
|
|
665
|
+
|
|
666
|
+
for d in msg.data:
|
|
667
|
+
index_price = IndexPrice(
|
|
668
|
+
exchange=self._exchange_id,
|
|
669
|
+
symbol=symbol,
|
|
670
|
+
price=float(d.idxPx),
|
|
671
|
+
timestamp=int(d.ts),
|
|
672
|
+
)
|
|
673
|
+
self._msgbus.publish(topic="index_price", msg=index_price)
|
|
674
|
+
|
|
675
|
+
def _handle_mark_price(self, raw: bytes):
|
|
676
|
+
msg: OkxWsMarkPriceMsg = self._ws_msg_mark_price_decoder.decode(raw)
|
|
677
|
+
|
|
678
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
679
|
+
symbol = self._market_id[id]
|
|
680
|
+
|
|
681
|
+
for d in msg.data:
|
|
682
|
+
mark_price = MarkPrice(
|
|
683
|
+
exchange=self._exchange_id,
|
|
684
|
+
symbol=symbol,
|
|
685
|
+
price=float(d.markPx),
|
|
686
|
+
timestamp=int(d.ts),
|
|
687
|
+
)
|
|
688
|
+
self._msgbus.publish(topic="mark_price", msg=mark_price)
|
|
689
|
+
|
|
690
|
+
def _handle_funding_rate(self, raw: bytes):
|
|
691
|
+
msg: OkxWsFundingRateMsg = self._ws_msg_funding_rate_decoder.decode(raw)
|
|
692
|
+
|
|
693
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
694
|
+
symbol = self._market_id[id]
|
|
695
|
+
|
|
696
|
+
for d in msg.data:
|
|
697
|
+
funding_rate = FundingRate(
|
|
698
|
+
exchange=self._exchange_id,
|
|
699
|
+
symbol=symbol,
|
|
700
|
+
rate=float(d.fundingRate),
|
|
701
|
+
timestamp=int(d.ts),
|
|
702
|
+
next_funding_time=int(d.fundingTime),
|
|
703
|
+
)
|
|
704
|
+
self._msgbus.publish(topic="funding_rate", msg=funding_rate)
|
|
705
|
+
|
|
706
|
+
def _handle_book5(self, raw: bytes):
|
|
707
|
+
msg: OkxWsBook5Msg = self._ws_msg_book5_decoder.decode(raw)
|
|
708
|
+
|
|
709
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
710
|
+
symbol = self._market_id[id]
|
|
711
|
+
|
|
712
|
+
for d in msg.data:
|
|
713
|
+
asks = [d.parse_to_book_order_data() for d in d.asks]
|
|
714
|
+
bids = [d.parse_to_book_order_data() for d in d.bids]
|
|
715
|
+
bookl2 = BookL2(
|
|
716
|
+
exchange=self._exchange_id,
|
|
717
|
+
symbol=symbol,
|
|
718
|
+
asks=asks,
|
|
719
|
+
bids=bids,
|
|
720
|
+
timestamp=int(d.ts),
|
|
721
|
+
)
|
|
722
|
+
self._msgbus.publish(topic="bookl2", msg=bookl2)
|
|
723
|
+
|
|
724
|
+
def _handle_books(self, raw: bytes):
|
|
725
|
+
msg: OkxWsBooksMsg = self._ws_msg_books_decoder.decode(raw)
|
|
726
|
+
|
|
727
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
728
|
+
symbol = self._market_id[id]
|
|
729
|
+
ob = self._okx_orderbook[symbol]
|
|
730
|
+
|
|
731
|
+
for d in msg.data:
|
|
732
|
+
if msg.action == "snapshot":
|
|
733
|
+
ob.apply_snapshot(d)
|
|
734
|
+
else:
|
|
735
|
+
ob.apply_update(d)
|
|
736
|
+
result = ob.get_orderbook()
|
|
737
|
+
bookl2 = BookL2(
|
|
738
|
+
exchange=self._exchange_id,
|
|
739
|
+
symbol=symbol,
|
|
740
|
+
bids=result["bids"],
|
|
741
|
+
asks=result["asks"],
|
|
742
|
+
timestamp=int(d.ts),
|
|
743
|
+
)
|
|
744
|
+
self._msgbus.publish(topic="bookl2", msg=bookl2)
|
|
745
|
+
|
|
746
|
+
def _handle_event_msg(self, ws_msg: OkxWsGeneralMsg):
|
|
747
|
+
if ws_msg.event == "error":
|
|
748
|
+
self._log.error(f"Error code: {ws_msg.code}, message: {ws_msg.msg}")
|
|
749
|
+
elif ws_msg.event == "login":
|
|
750
|
+
self._log.debug("Login success")
|
|
751
|
+
elif ws_msg.event == "subscribe":
|
|
752
|
+
self._log.debug(f"Subscribed to {self._require_channel(ws_msg)}")
|
|
753
|
+
|
|
754
|
+
def _handle_kline(self, raw: bytes):
|
|
755
|
+
msg: OkxWsCandleMsg = self._ws_msg_candle_decoder.decode(raw)
|
|
756
|
+
|
|
757
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
758
|
+
symbol = self._market_id[id]
|
|
759
|
+
okx_interval = OkxKlineInterval(self._require_arg_channel(msg.arg.channel))
|
|
760
|
+
interval = OkxEnumParser.parse_kline_interval(okx_interval)
|
|
761
|
+
|
|
762
|
+
for d in msg.data:
|
|
763
|
+
kline = Kline(
|
|
764
|
+
exchange=self._exchange_id,
|
|
765
|
+
symbol=symbol,
|
|
766
|
+
interval=interval,
|
|
767
|
+
open=float(d[1]),
|
|
768
|
+
high=float(d[2]),
|
|
769
|
+
low=float(d[3]),
|
|
770
|
+
close=float(d[4]),
|
|
771
|
+
volume=float(d[5]),
|
|
772
|
+
start=int(d[0]),
|
|
773
|
+
timestamp=self._clock.timestamp_ms(),
|
|
774
|
+
confirm=False if d[8] == "0" else True,
|
|
775
|
+
)
|
|
776
|
+
self._msgbus.publish(topic="kline", msg=kline)
|
|
777
|
+
|
|
778
|
+
def _handle_trade(self, raw: bytes):
|
|
779
|
+
msg: OkxWsTradeMsg = self._ws_msg_trade_decoder.decode(raw)
|
|
780
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
781
|
+
symbol = self._market_id[id]
|
|
782
|
+
for d in msg.data:
|
|
783
|
+
trade = Trade(
|
|
784
|
+
exchange=self._exchange_id,
|
|
785
|
+
symbol=symbol,
|
|
786
|
+
price=float(d.px),
|
|
787
|
+
size=float(d.sz),
|
|
788
|
+
timestamp=int(d.ts),
|
|
789
|
+
side=OkxEnumParser.parse_order_side(d.side),
|
|
790
|
+
)
|
|
791
|
+
self._msgbus.publish(topic="trade", msg=trade)
|
|
792
|
+
|
|
793
|
+
def _handle_bbo_tbt(self, raw: bytes):
|
|
794
|
+
msg: OkxWsBboTbtMsg = self._ws_msg_bbo_tbt_decoder.decode(raw)
|
|
795
|
+
|
|
796
|
+
id = self._require_inst_id(msg.arg.instId)
|
|
797
|
+
symbol = self._market_id[id]
|
|
798
|
+
|
|
799
|
+
for d in msg.data:
|
|
800
|
+
if not d.bids or not d.asks:
|
|
801
|
+
continue
|
|
802
|
+
|
|
803
|
+
bookl1 = BookL1(
|
|
804
|
+
exchange=self._exchange_id,
|
|
805
|
+
symbol=symbol,
|
|
806
|
+
bid=float(d.bids[0][0]),
|
|
807
|
+
ask=float(d.asks[0][0]),
|
|
808
|
+
bid_size=float(d.bids[0][1]),
|
|
809
|
+
ask_size=float(d.asks[0][1]),
|
|
810
|
+
timestamp=int(d.ts),
|
|
811
|
+
)
|
|
812
|
+
self._msgbus.publish(topic="bookl1", msg=bookl1)
|
|
813
|
+
|
|
814
|
+
def _handle_index_candlesticks(
|
|
815
|
+
self,
|
|
816
|
+
symbol: str,
|
|
817
|
+
interval: KlineInterval,
|
|
818
|
+
kline: OkxIndexCandlesticksResponseData,
|
|
819
|
+
) -> Kline:
|
|
820
|
+
return Kline(
|
|
821
|
+
exchange=self._exchange_id,
|
|
822
|
+
symbol=symbol,
|
|
823
|
+
interval=interval,
|
|
824
|
+
open=float(kline.o),
|
|
825
|
+
high=float(kline.h),
|
|
826
|
+
low=float(kline.l),
|
|
827
|
+
close=float(kline.c),
|
|
828
|
+
start=int(kline.ts),
|
|
829
|
+
timestamp=self._clock.timestamp_ms(),
|
|
830
|
+
confirm=False if int(kline.confirm) == 0 else True,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
def _handle_candlesticks(
|
|
834
|
+
self, symbol: str, interval: KlineInterval, kline: OkxCandlesticksResponseData
|
|
835
|
+
) -> Kline:
|
|
836
|
+
return Kline(
|
|
837
|
+
exchange=self._exchange_id,
|
|
838
|
+
symbol=symbol,
|
|
839
|
+
interval=interval,
|
|
840
|
+
open=float(kline.o),
|
|
841
|
+
high=float(kline.h),
|
|
842
|
+
low=float(kline.l),
|
|
843
|
+
close=float(kline.c),
|
|
844
|
+
volume=float(kline.vol),
|
|
845
|
+
quote_volume=float(kline.volCcyQuote),
|
|
846
|
+
start=int(kline.ts),
|
|
847
|
+
timestamp=self._clock.timestamp_ms(),
|
|
848
|
+
confirm=False if int(kline.confirm) == 0 else True,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
async def connect(self):
|
|
852
|
+
await self._ws_client.connect()
|
|
853
|
+
await self._business_ws_client.connect()
|
|
854
|
+
|
|
855
|
+
async def wait_ready(self):
|
|
856
|
+
"""Wait for the initial WebSocket connection to be established"""
|
|
857
|
+
await self._ws_client.wait_ready()
|
|
858
|
+
await self._business_ws_client.wait_ready()
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class OkxPrivateConnector(PrivateConnector):
|
|
862
|
+
_api_client: OkxApiClient
|
|
863
|
+
_account_type: OkxAccountType
|
|
864
|
+
_market: Dict[str, OkxMarket]
|
|
865
|
+
_market_id: Dict[str, str]
|
|
866
|
+
_oms: OkxOrderManagementSystem
|
|
867
|
+
|
|
868
|
+
def __init__(
|
|
869
|
+
self,
|
|
870
|
+
account_type: OkxAccountType,
|
|
871
|
+
exchange: OkxExchangeManager,
|
|
872
|
+
cache: AsyncCache,
|
|
873
|
+
registry: OrderRegistry,
|
|
874
|
+
clock: LiveClock,
|
|
875
|
+
msgbus: MessageBus,
|
|
876
|
+
task_manager: TaskManager,
|
|
877
|
+
order_query_config: OrderQueryConfig,
|
|
878
|
+
enable_rate_limit: bool = True,
|
|
879
|
+
max_subscriptions_per_client: int | None = None,
|
|
880
|
+
max_clients: int | None = None,
|
|
881
|
+
**kwargs,
|
|
882
|
+
):
|
|
883
|
+
if not exchange.api_key or not exchange.secret or not exchange.passphrase:
|
|
884
|
+
raise ValueError(
|
|
885
|
+
"API key, secret, and passphrase are required for private endpoints"
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
api_client = OkxApiClient(
|
|
889
|
+
clock=clock,
|
|
890
|
+
api_key=exchange.api_key,
|
|
891
|
+
secret=exchange.secret,
|
|
892
|
+
passphrase=exchange.passphrase,
|
|
893
|
+
testnet=account_type.is_testnet,
|
|
894
|
+
enable_rate_limit=enable_rate_limit,
|
|
895
|
+
**kwargs,
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
oms = OkxOrderManagementSystem(
|
|
899
|
+
account_type=account_type,
|
|
900
|
+
api_key=exchange.api_key,
|
|
901
|
+
secret=exchange.secret,
|
|
902
|
+
passphrase=exchange.passphrase,
|
|
903
|
+
market=exchange.market,
|
|
904
|
+
market_id=exchange.market_id,
|
|
905
|
+
registry=registry,
|
|
906
|
+
cache=cache,
|
|
907
|
+
api_client=api_client,
|
|
908
|
+
exchange_id=exchange.exchange_id,
|
|
909
|
+
clock=clock,
|
|
910
|
+
msgbus=msgbus,
|
|
911
|
+
task_manager=task_manager,
|
|
912
|
+
enable_rate_limit=enable_rate_limit,
|
|
913
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
914
|
+
max_clients=max_clients,
|
|
915
|
+
order_query_config=order_query_config,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
super().__init__(
|
|
919
|
+
account_type=account_type,
|
|
920
|
+
market=exchange.market,
|
|
921
|
+
api_client=api_client,
|
|
922
|
+
task_manager=task_manager,
|
|
923
|
+
oms=oms,
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
async def connect(self):
|
|
927
|
+
self._oms._ws_client.subscribe_orders()
|
|
928
|
+
self._oms._ws_client.subscribe_positions()
|
|
929
|
+
self._oms._ws_client.subscribe_account()
|
|
930
|
+
await self._oms._ws_client.connect()
|
|
931
|
+
await self._oms._ws_api_client.connect()
|