vnpy_okx 2025.6.17__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.
vnpy_okx/okx_gateway.py
ADDED
|
@@ -0,0 +1,1502 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import hmac
|
|
4
|
+
import json
|
|
5
|
+
import time
|
|
6
|
+
from copy import copy
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
from types import TracebackType
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
from vnpy.event import EventEngine
|
|
13
|
+
from vnpy.trader.constant import (
|
|
14
|
+
Direction,
|
|
15
|
+
Exchange,
|
|
16
|
+
Interval,
|
|
17
|
+
Offset,
|
|
18
|
+
OrderType,
|
|
19
|
+
Product,
|
|
20
|
+
Status
|
|
21
|
+
)
|
|
22
|
+
from vnpy.trader.gateway import BaseGateway
|
|
23
|
+
from vnpy.trader.utility import round_to, ZoneInfo
|
|
24
|
+
from vnpy.trader.object import (
|
|
25
|
+
AccountData,
|
|
26
|
+
BarData,
|
|
27
|
+
CancelRequest,
|
|
28
|
+
ContractData,
|
|
29
|
+
HistoryRequest,
|
|
30
|
+
OrderData,
|
|
31
|
+
OrderRequest,
|
|
32
|
+
PositionData,
|
|
33
|
+
SubscribeRequest,
|
|
34
|
+
TickData,
|
|
35
|
+
TradeData
|
|
36
|
+
)
|
|
37
|
+
from vnpy_rest import Request, Response, RestClient
|
|
38
|
+
from vnpy_websocket import WebsocketClient
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# UTC timezone
|
|
42
|
+
CHINA_TZ: ZoneInfo = ZoneInfo("Asia/Shanghai")
|
|
43
|
+
|
|
44
|
+
# Real server hosts
|
|
45
|
+
REAL_REST_HOST: str = "https://www.okx.com"
|
|
46
|
+
REAL_PUBLIC_HOST: str = "wss://ws.okx.com:8443/ws/v5/public"
|
|
47
|
+
REAL_PRIVATE_HOST: str = "wss://ws.okx.com:8443/ws/v5/private"
|
|
48
|
+
REAL_BUSINESS_HOST: str = "wss://ws.okx.com:8443/ws/v5/business"
|
|
49
|
+
|
|
50
|
+
# AWS server hosts
|
|
51
|
+
AWS_REST_HOST: str = "https://aws.okx.com"
|
|
52
|
+
AWS_PUBLIC_HOST: str = "wss://wsaws.okx.com:8443/ws/v5/public"
|
|
53
|
+
AWS_PRIVATE_HOST: str = "wss://wsaws.okx.com:8443/ws/v5/private"
|
|
54
|
+
AWS_BUSINESS_HOST: str = "wss://wsaws.okx.com:8443/ws/v5/business"
|
|
55
|
+
|
|
56
|
+
# Demo server hosts
|
|
57
|
+
DEMO_REST_HOST: str = "https://www.okx.com"
|
|
58
|
+
DEMO_PUBLIC_HOST: str = "wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999"
|
|
59
|
+
DEMO_PRIVATE_HOST: str = "wss://wspap.okx.com:8443/ws/v5/private?brokerId=9999"
|
|
60
|
+
DEMO_BUSINESS_HOST: str = "wss://wspap.okx.com:8443/ws/v5/business?brokerId=9999"
|
|
61
|
+
|
|
62
|
+
# Order status map
|
|
63
|
+
STATUS_OKX2VT: dict[str, Status] = {
|
|
64
|
+
"live": Status.NOTTRADED,
|
|
65
|
+
"partially_filled": Status.PARTTRADED,
|
|
66
|
+
"filled": Status.ALLTRADED,
|
|
67
|
+
"canceled": Status.CANCELLED,
|
|
68
|
+
"mmp_canceled": Status.CANCELLED
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# Order type map
|
|
72
|
+
ORDERTYPE_OKX2VT: dict[str, OrderType] = {
|
|
73
|
+
"limit": OrderType.LIMIT,
|
|
74
|
+
"fok": OrderType.FOK,
|
|
75
|
+
"ioc": OrderType.FAK
|
|
76
|
+
}
|
|
77
|
+
ORDERTYPE_VT2OKX: dict[OrderType, str] = {v: k for k, v in ORDERTYPE_OKX2VT.items()}
|
|
78
|
+
|
|
79
|
+
# Direction map
|
|
80
|
+
DIRECTION_OKX2VT: dict[str, Direction] = {
|
|
81
|
+
"buy": Direction.LONG,
|
|
82
|
+
"sell": Direction.SHORT
|
|
83
|
+
}
|
|
84
|
+
DIRECTION_VT2OKX: dict[Direction, str] = {v: k for k, v in DIRECTION_OKX2VT.items()}
|
|
85
|
+
|
|
86
|
+
# Kline interval map
|
|
87
|
+
INTERVAL_VT2OKX: dict[Interval, str] = {
|
|
88
|
+
Interval.MINUTE: "1m",
|
|
89
|
+
Interval.HOUR: "1H",
|
|
90
|
+
Interval.DAILY: "1D",
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Product type map
|
|
94
|
+
PRODUCT_OKX2VT: dict[str, Product] = {
|
|
95
|
+
"SWAP": Product.SWAP,
|
|
96
|
+
"SPOT": Product.SPOT,
|
|
97
|
+
"FUTURES": Product.FUTURES
|
|
98
|
+
}
|
|
99
|
+
PRODUCT_VT2OKX: dict[Product, str] = {v: k for k, v in PRODUCT_OKX2VT.items()}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class OkxGateway(BaseGateway):
|
|
103
|
+
"""
|
|
104
|
+
The OKX trading gateway for VeighNa.
|
|
105
|
+
|
|
106
|
+
Only support net mode
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
default_name = "OKX"
|
|
110
|
+
|
|
111
|
+
default_setting: dict = {
|
|
112
|
+
"API Key": "",
|
|
113
|
+
"Secret Key": "",
|
|
114
|
+
"Passphrase": "",
|
|
115
|
+
"Server": ["REAL", "AWS", "DEMO"],
|
|
116
|
+
"Proxy Host": "",
|
|
117
|
+
"Proxy Port": 0,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
exchanges: Exchange = [Exchange.GLOBAL]
|
|
121
|
+
|
|
122
|
+
def __init__(self, event_engine: EventEngine, gateway_name: str) -> None:
|
|
123
|
+
"""
|
|
124
|
+
The init method of the gateway.
|
|
125
|
+
|
|
126
|
+
event_engine: the global event engine object of VeighNa
|
|
127
|
+
gateway_name: the unique name for identifying the gateway
|
|
128
|
+
"""
|
|
129
|
+
super().__init__(event_engine, gateway_name)
|
|
130
|
+
|
|
131
|
+
self.key: str = ""
|
|
132
|
+
self.secret: str = ""
|
|
133
|
+
self.passphrase: str = ""
|
|
134
|
+
self.server: str = ""
|
|
135
|
+
self.proxy_host: str = ""
|
|
136
|
+
self.proxy_port: int = 0
|
|
137
|
+
|
|
138
|
+
self.orders: dict[str, OrderData] = {}
|
|
139
|
+
self.local_orderids: set[str] = set()
|
|
140
|
+
|
|
141
|
+
self.symbol_contract_map: dict[str, ContractData] = {}
|
|
142
|
+
self.name_contract_map: dict[str, ContractData] = {}
|
|
143
|
+
|
|
144
|
+
self.rest_api: RestApi = RestApi(self)
|
|
145
|
+
self.public_api: PublicApi = PublicApi(self)
|
|
146
|
+
self.private_api: PrivateApi = PrivateApi(self)
|
|
147
|
+
|
|
148
|
+
def connect(self, setting: dict) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Start server connections.
|
|
151
|
+
|
|
152
|
+
This method establishes connections to OKX servers
|
|
153
|
+
using the provided settings.
|
|
154
|
+
|
|
155
|
+
Parameters:
|
|
156
|
+
setting: A dictionary containing connection parameters including
|
|
157
|
+
API credentials, server selection, and proxy configuration
|
|
158
|
+
"""
|
|
159
|
+
self.key = setting["API Key"]
|
|
160
|
+
self.secret = setting["Secret Key"]
|
|
161
|
+
self.passphrase = setting["Passphrase"]
|
|
162
|
+
self.server = setting["Server"]
|
|
163
|
+
self.proxy_host = setting["Proxy Host"]
|
|
164
|
+
self.proxy_port = setting["Proxy Port"]
|
|
165
|
+
|
|
166
|
+
self.rest_api.connect(
|
|
167
|
+
self.key,
|
|
168
|
+
self.secret,
|
|
169
|
+
self.passphrase,
|
|
170
|
+
self.server,
|
|
171
|
+
self.proxy_host,
|
|
172
|
+
self.proxy_port
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def connect_ws_api(self) -> None:
|
|
176
|
+
"""
|
|
177
|
+
Connect to OKX websocket API.
|
|
178
|
+
"""
|
|
179
|
+
self.public_api.connect(
|
|
180
|
+
self.server,
|
|
181
|
+
self.proxy_host,
|
|
182
|
+
self.proxy_port,
|
|
183
|
+
)
|
|
184
|
+
self.private_api.connect(
|
|
185
|
+
self.key,
|
|
186
|
+
self.secret,
|
|
187
|
+
self.passphrase,
|
|
188
|
+
self.server,
|
|
189
|
+
self.proxy_host,
|
|
190
|
+
self.proxy_port,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def subscribe(self, req: SubscribeRequest) -> None:
|
|
194
|
+
"""
|
|
195
|
+
Subscribe to market data.
|
|
196
|
+
|
|
197
|
+
Parameters:
|
|
198
|
+
req: Subscription request object containing symbol information
|
|
199
|
+
"""
|
|
200
|
+
self.public_api.subscribe(req)
|
|
201
|
+
|
|
202
|
+
def send_order(self, req: OrderRequest) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Send new order to OKX.
|
|
205
|
+
|
|
206
|
+
This function delegates order placement to the private websocket API,
|
|
207
|
+
which handles validation, order generation, and submission to the exchange.
|
|
208
|
+
|
|
209
|
+
Parameters:
|
|
210
|
+
req: Order request object containing order details
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
str: The VeighNa order ID if successful, empty string otherwise
|
|
214
|
+
"""
|
|
215
|
+
return self.private_api.send_order(req)
|
|
216
|
+
|
|
217
|
+
def cancel_order(self, req: CancelRequest) -> None:
|
|
218
|
+
"""
|
|
219
|
+
Cancel existing order on OKX.
|
|
220
|
+
|
|
221
|
+
This function delegates order cancellation to the private websocket API,
|
|
222
|
+
which determines the appropriate ID type to use and submits the request.
|
|
223
|
+
|
|
224
|
+
Parameters:
|
|
225
|
+
req: Cancel request object containing order details
|
|
226
|
+
"""
|
|
227
|
+
self.private_api.cancel_order(req)
|
|
228
|
+
|
|
229
|
+
def query_account(self) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Not required since OKX provides websocket update for account balances.
|
|
232
|
+
"""
|
|
233
|
+
pass
|
|
234
|
+
|
|
235
|
+
def query_position(self) -> None:
|
|
236
|
+
"""
|
|
237
|
+
Not required since OKX provides websocket update for positions.
|
|
238
|
+
"""
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def query_history(self, req: HistoryRequest) -> list[BarData]:
|
|
242
|
+
"""
|
|
243
|
+
Query historical kline data.
|
|
244
|
+
|
|
245
|
+
Parameters:
|
|
246
|
+
req: History request object containing query parameters
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
list[BarData]: List of historical kline data bars
|
|
250
|
+
"""
|
|
251
|
+
return self.rest_api.query_history(req)
|
|
252
|
+
|
|
253
|
+
def close(self) -> None:
|
|
254
|
+
"""
|
|
255
|
+
Close server connections.
|
|
256
|
+
|
|
257
|
+
This method stops all API connections and releases resources.
|
|
258
|
+
"""
|
|
259
|
+
self.rest_api.stop()
|
|
260
|
+
self.public_api.stop()
|
|
261
|
+
self.private_api.stop()
|
|
262
|
+
|
|
263
|
+
def on_order(self, order: OrderData) -> None:
|
|
264
|
+
"""
|
|
265
|
+
Save a copy of order and then push to event engine.
|
|
266
|
+
|
|
267
|
+
Parameters:
|
|
268
|
+
order: Order data object
|
|
269
|
+
"""
|
|
270
|
+
self.orders[order.orderid] = order
|
|
271
|
+
super().on_order(order)
|
|
272
|
+
|
|
273
|
+
def get_order(self, orderid: str) -> OrderData:
|
|
274
|
+
"""
|
|
275
|
+
Get previously saved order by order id.
|
|
276
|
+
|
|
277
|
+
Parameters:
|
|
278
|
+
orderid: The ID of the order to retrieve
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
OrderData: Order data object if found, None otherwise
|
|
282
|
+
"""
|
|
283
|
+
return self.orders.get(orderid, None)
|
|
284
|
+
|
|
285
|
+
def on_contract(self, contract: ContractData) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Save a copy of contract and then push to event engine.
|
|
288
|
+
|
|
289
|
+
Parameters:
|
|
290
|
+
contract: Contract data object
|
|
291
|
+
"""
|
|
292
|
+
self.symbol_contract_map[contract.symbol] = contract
|
|
293
|
+
self.name_contract_map[contract.name] = contract
|
|
294
|
+
|
|
295
|
+
super().on_contract(contract)
|
|
296
|
+
|
|
297
|
+
def get_contract_by_symbol(self, symbol: str) -> ContractData | None:
|
|
298
|
+
"""
|
|
299
|
+
Get contract by VeighNa symbol.
|
|
300
|
+
|
|
301
|
+
Parameters:
|
|
302
|
+
symbol: The symbol of the contract
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
ContractData: Contract data object if found, None otherwise
|
|
306
|
+
"""
|
|
307
|
+
return self.symbol_contract_map.get(symbol, None)
|
|
308
|
+
|
|
309
|
+
def get_contract_by_name(self, name: str) -> ContractData | None:
|
|
310
|
+
"""
|
|
311
|
+
Get contract by exchange symbol name.
|
|
312
|
+
|
|
313
|
+
Parameters:
|
|
314
|
+
name: The name of the contract
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
ContractData: Contract data object if found, None otherwise
|
|
318
|
+
"""
|
|
319
|
+
return self.name_contract_map.get(name, None)
|
|
320
|
+
|
|
321
|
+
def parse_order_data(self, data: dict, gateway_name: str) -> OrderData:
|
|
322
|
+
"""
|
|
323
|
+
Parse dict to order data.
|
|
324
|
+
|
|
325
|
+
This function converts OKX order data into a VeighNa OrderData object.
|
|
326
|
+
It extracts and maps all relevant fields from the exchange response.
|
|
327
|
+
|
|
328
|
+
Parameters:
|
|
329
|
+
data: Order data from OKX
|
|
330
|
+
gateway_name: Gateway name for identification
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
OrderData: VeighNa order object
|
|
334
|
+
"""
|
|
335
|
+
contract: ContractData = self.get_contract_by_name(data["instId"])
|
|
336
|
+
|
|
337
|
+
order_id: str = data["clOrdId"]
|
|
338
|
+
if order_id:
|
|
339
|
+
self.local_orderids.add(order_id)
|
|
340
|
+
else:
|
|
341
|
+
order_id = data["ordId"]
|
|
342
|
+
|
|
343
|
+
order: OrderData = OrderData(
|
|
344
|
+
symbol=contract.symbol,
|
|
345
|
+
exchange=Exchange.GLOBAL,
|
|
346
|
+
type=ORDERTYPE_OKX2VT[data["ordType"]],
|
|
347
|
+
orderid=order_id,
|
|
348
|
+
direction=DIRECTION_OKX2VT[data["side"]],
|
|
349
|
+
offset=Offset.NONE,
|
|
350
|
+
traded=float(data["accFillSz"]),
|
|
351
|
+
price=float(data["px"]),
|
|
352
|
+
volume=float(data["sz"]),
|
|
353
|
+
datetime=parse_timestamp(data["cTime"]),
|
|
354
|
+
status=STATUS_OKX2VT[data["state"]],
|
|
355
|
+
gateway_name=gateway_name,
|
|
356
|
+
)
|
|
357
|
+
return order
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
class RestApi(RestClient):
|
|
361
|
+
"""The REST API of OkxGateway"""
|
|
362
|
+
|
|
363
|
+
def __init__(self, gateway: OkxGateway) -> None:
|
|
364
|
+
"""
|
|
365
|
+
The init method of the api.
|
|
366
|
+
|
|
367
|
+
Parameters:
|
|
368
|
+
gateway: the parent gateway object for pushing callback data.
|
|
369
|
+
"""
|
|
370
|
+
super().__init__()
|
|
371
|
+
|
|
372
|
+
self.gateway: OkxGateway = gateway
|
|
373
|
+
self.gateway_name: str = gateway.gateway_name
|
|
374
|
+
|
|
375
|
+
self.key: str = ""
|
|
376
|
+
self.secret: bytes = b""
|
|
377
|
+
self.passphrase: str = ""
|
|
378
|
+
self.simulated: bool = False
|
|
379
|
+
|
|
380
|
+
self.product_ready: set = set()
|
|
381
|
+
|
|
382
|
+
def sign(self, request: Request) -> Request:
|
|
383
|
+
"""
|
|
384
|
+
Standard callback for signing a request.
|
|
385
|
+
|
|
386
|
+
This method adds the necessary authentication parameters and signature
|
|
387
|
+
to requests that require API key authentication.
|
|
388
|
+
|
|
389
|
+
Parameters:
|
|
390
|
+
request: Request object to be signed
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Request: Modified request with authentication parameters
|
|
394
|
+
"""
|
|
395
|
+
# Public API does not need to sign
|
|
396
|
+
if "public" in request.path:
|
|
397
|
+
return request
|
|
398
|
+
|
|
399
|
+
# Generate signature
|
|
400
|
+
timestamp: str = generate_timestamp()
|
|
401
|
+
|
|
402
|
+
if request.data:
|
|
403
|
+
request.data = json.dumps(request.data)
|
|
404
|
+
else:
|
|
405
|
+
request.data = ""
|
|
406
|
+
|
|
407
|
+
if request.params:
|
|
408
|
+
path: str = request.path + "?" + urlencode(request.params)
|
|
409
|
+
else:
|
|
410
|
+
path = request.path
|
|
411
|
+
|
|
412
|
+
msg: str = timestamp + request.method + path + request.data
|
|
413
|
+
signature: bytes = generate_signature(msg, self.secret)
|
|
414
|
+
|
|
415
|
+
# Add request header
|
|
416
|
+
request.headers = {
|
|
417
|
+
"OK-ACCESS-KEY": self.key,
|
|
418
|
+
"OK-ACCESS-SIGN": signature.decode(),
|
|
419
|
+
"OK-ACCESS-TIMESTAMP": timestamp,
|
|
420
|
+
"OK-ACCESS-PASSPHRASE": self.passphrase,
|
|
421
|
+
"Content-Type": "application/json"
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if self.simulated:
|
|
425
|
+
request.headers["x-simulated-trading"] = "1"
|
|
426
|
+
|
|
427
|
+
return request
|
|
428
|
+
|
|
429
|
+
def connect(
|
|
430
|
+
self,
|
|
431
|
+
key: str,
|
|
432
|
+
secret: str,
|
|
433
|
+
passphrase: str,
|
|
434
|
+
server: str,
|
|
435
|
+
proxy_host: str,
|
|
436
|
+
proxy_port: int,
|
|
437
|
+
) -> None:
|
|
438
|
+
"""
|
|
439
|
+
Start server connection.
|
|
440
|
+
|
|
441
|
+
This method establishes a connection to OKX REST API server
|
|
442
|
+
using the provided credentials and configuration.
|
|
443
|
+
|
|
444
|
+
Parameters:
|
|
445
|
+
key: API Key for authentication
|
|
446
|
+
secret: API Secret for request signing
|
|
447
|
+
passphrase: API Passphrase for authentication
|
|
448
|
+
server: Server type ("REAL", "AWS", or "DEMO")
|
|
449
|
+
proxy_host: Proxy server hostname or IP
|
|
450
|
+
proxy_port: Proxy server port
|
|
451
|
+
"""
|
|
452
|
+
self.key = key
|
|
453
|
+
self.secret = secret.encode()
|
|
454
|
+
self.passphrase = passphrase
|
|
455
|
+
|
|
456
|
+
if server == "DEMO":
|
|
457
|
+
self.simulated = True
|
|
458
|
+
|
|
459
|
+
self.connect_time = int(datetime.now().strftime("%y%m%d%H%M%S"))
|
|
460
|
+
|
|
461
|
+
server_hosts: dict[str, str] = {
|
|
462
|
+
"REAL": REAL_REST_HOST,
|
|
463
|
+
"AWS": AWS_REST_HOST,
|
|
464
|
+
"DEMO": DEMO_REST_HOST,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
host: str = server_hosts[server]
|
|
468
|
+
self.init(host, proxy_host, proxy_port)
|
|
469
|
+
|
|
470
|
+
self.start()
|
|
471
|
+
self.gateway.write_log("REST API started")
|
|
472
|
+
|
|
473
|
+
self.query_time()
|
|
474
|
+
self.query_contract()
|
|
475
|
+
|
|
476
|
+
def query_time(self) -> None:
|
|
477
|
+
"""
|
|
478
|
+
Query server time.
|
|
479
|
+
|
|
480
|
+
This function sends a request to get the exchange server time,
|
|
481
|
+
which is used to synchronize local time with server time.
|
|
482
|
+
"""
|
|
483
|
+
self.add_request(
|
|
484
|
+
"GET",
|
|
485
|
+
"/api/v5/public/time",
|
|
486
|
+
callback=self.on_query_time
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
def query_order(self) -> None:
|
|
490
|
+
"""
|
|
491
|
+
Query open orders.
|
|
492
|
+
|
|
493
|
+
This function sends a request to get all active orders
|
|
494
|
+
that have not been fully filled or cancelled.
|
|
495
|
+
"""
|
|
496
|
+
self.add_request(
|
|
497
|
+
"GET",
|
|
498
|
+
"/api/v5/trade/orders-pending",
|
|
499
|
+
callback=self.on_query_order,
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
def query_contract(self) -> None:
|
|
503
|
+
"""
|
|
504
|
+
Query available contracts.
|
|
505
|
+
|
|
506
|
+
This function sends a request to get exchange information,
|
|
507
|
+
including all available trading instruments and their specifications.
|
|
508
|
+
"""
|
|
509
|
+
for inst_type in PRODUCT_OKX2VT.keys():
|
|
510
|
+
self.add_request(
|
|
511
|
+
"GET",
|
|
512
|
+
"/api/v5/public/instruments",
|
|
513
|
+
callback=self.on_query_contract,
|
|
514
|
+
params={"instType": inst_type}
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def on_query_time(self, packet: dict, request: Request) -> None:
|
|
518
|
+
"""
|
|
519
|
+
Callback of server time query.
|
|
520
|
+
|
|
521
|
+
This function processes the server time response and calculates
|
|
522
|
+
the time difference between local and server time for logging.
|
|
523
|
+
|
|
524
|
+
Parameters:
|
|
525
|
+
packet: Response data from the server
|
|
526
|
+
request: Original request object
|
|
527
|
+
"""
|
|
528
|
+
timestamp: int = int(packet["data"][0]["ts"])
|
|
529
|
+
server_time: datetime = datetime.fromtimestamp(timestamp / 1000)
|
|
530
|
+
local_time: datetime = datetime.now()
|
|
531
|
+
|
|
532
|
+
msg: str = f"Server time: {server_time}, local time: {local_time}"
|
|
533
|
+
self.gateway.write_log(msg)
|
|
534
|
+
|
|
535
|
+
def on_query_order(self, packet: dict, request: Request) -> None:
|
|
536
|
+
"""
|
|
537
|
+
Callback of open orders query.
|
|
538
|
+
|
|
539
|
+
This function processes the open orders response and
|
|
540
|
+
creates OrderData objects for each active order.
|
|
541
|
+
|
|
542
|
+
Parameters:
|
|
543
|
+
packet: Response data from the server
|
|
544
|
+
request: Original request object
|
|
545
|
+
"""
|
|
546
|
+
for order_info in packet["data"]:
|
|
547
|
+
order: OrderData = self.gateway.parse_order_data(
|
|
548
|
+
order_info,
|
|
549
|
+
self.gateway_name
|
|
550
|
+
)
|
|
551
|
+
self.gateway.on_order(order)
|
|
552
|
+
|
|
553
|
+
self.gateway.write_log("Order data received")
|
|
554
|
+
|
|
555
|
+
def on_query_contract(self, packet: dict, request: Request) -> None:
|
|
556
|
+
"""
|
|
557
|
+
Callback of available contracts query.
|
|
558
|
+
|
|
559
|
+
This function processes the exchange info response and
|
|
560
|
+
creates ContractData objects for each trading instrument.
|
|
561
|
+
|
|
562
|
+
Parameters:
|
|
563
|
+
packet: Response data from the server
|
|
564
|
+
request: Original request object
|
|
565
|
+
"""
|
|
566
|
+
data: list = packet["data"]
|
|
567
|
+
|
|
568
|
+
for d in data:
|
|
569
|
+
name: str = d["instId"]
|
|
570
|
+
product: Product = PRODUCT_OKX2VT[d["instType"]]
|
|
571
|
+
net_position: bool = True
|
|
572
|
+
|
|
573
|
+
if product == Product.SPOT:
|
|
574
|
+
size: float = 1
|
|
575
|
+
else:
|
|
576
|
+
size = float(d["ctMult"])
|
|
577
|
+
|
|
578
|
+
match product:
|
|
579
|
+
case Product.SPOT:
|
|
580
|
+
symbol: str = name.replace("-", "") + "_SPOT_OKX"
|
|
581
|
+
case Product.SWAP:
|
|
582
|
+
base, quote, _ = name.split("-")
|
|
583
|
+
symbol = base + quote + "_SWAP_OKX"
|
|
584
|
+
case Product.FUTURES:
|
|
585
|
+
base, quote, expiry = name.split("-")
|
|
586
|
+
symbol = base + quote + "_" + expiry + "_OKX"
|
|
587
|
+
|
|
588
|
+
if d["tickSz"]:
|
|
589
|
+
pricetick: float = float(d["tickSz"])
|
|
590
|
+
else:
|
|
591
|
+
pricetick = 0
|
|
592
|
+
|
|
593
|
+
if d["minSz"]:
|
|
594
|
+
min_volume: float = float(d["minSz"])
|
|
595
|
+
else:
|
|
596
|
+
min_volume = 0
|
|
597
|
+
|
|
598
|
+
contract: ContractData = ContractData(
|
|
599
|
+
symbol=symbol,
|
|
600
|
+
exchange=Exchange.GLOBAL,
|
|
601
|
+
name=name,
|
|
602
|
+
product=product,
|
|
603
|
+
size=size,
|
|
604
|
+
pricetick=pricetick,
|
|
605
|
+
min_volume=min_volume,
|
|
606
|
+
history_data=True,
|
|
607
|
+
net_position=net_position,
|
|
608
|
+
gateway_name=self.gateway_name,
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
self.gateway.on_contract(contract)
|
|
612
|
+
|
|
613
|
+
self.gateway.write_log(f"{d['instType']} contract data received")
|
|
614
|
+
|
|
615
|
+
# Connect to websocket API after all contract data received
|
|
616
|
+
self.product_ready.add(contract.product)
|
|
617
|
+
|
|
618
|
+
if len(self.product_ready) == len(PRODUCT_OKX2VT):
|
|
619
|
+
self.query_order()
|
|
620
|
+
|
|
621
|
+
self.gateway.connect_ws_api()
|
|
622
|
+
|
|
623
|
+
def on_error(
|
|
624
|
+
self,
|
|
625
|
+
exc: type,
|
|
626
|
+
value: Exception,
|
|
627
|
+
tb: TracebackType,
|
|
628
|
+
request: Request
|
|
629
|
+
) -> None:
|
|
630
|
+
"""
|
|
631
|
+
General error callback.
|
|
632
|
+
|
|
633
|
+
This function is called when an exception occurs in REST API requests.
|
|
634
|
+
It logs the exception details for troubleshooting.
|
|
635
|
+
|
|
636
|
+
Parameters:
|
|
637
|
+
exc: Type of the exception
|
|
638
|
+
value: Exception instance
|
|
639
|
+
tb: Traceback object
|
|
640
|
+
request: Original request object
|
|
641
|
+
"""
|
|
642
|
+
detail: str = self.exception_detail(exc, value, tb, request)
|
|
643
|
+
|
|
644
|
+
msg: str = f"Exception catched by REST API: {detail}"
|
|
645
|
+
self.gateway.write_log(msg)
|
|
646
|
+
|
|
647
|
+
def query_history(self, req: HistoryRequest) -> list[BarData]:
|
|
648
|
+
"""
|
|
649
|
+
Query kline history data.
|
|
650
|
+
|
|
651
|
+
This function sends requests to get historical kline data
|
|
652
|
+
for a specific trading instrument and time period.
|
|
653
|
+
|
|
654
|
+
Parameters:
|
|
655
|
+
req: History request object containing query parameters
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
list[BarData]: List of historical kline data bars
|
|
659
|
+
"""
|
|
660
|
+
# Validate symbol exists in contract map
|
|
661
|
+
contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
|
|
662
|
+
if not contract:
|
|
663
|
+
self.gateway.write_log(f"Query kline history failed, symbol not found: {req.symbol}")
|
|
664
|
+
return []
|
|
665
|
+
|
|
666
|
+
# Initialize buffer for storing bars
|
|
667
|
+
buf: dict[datetime, BarData] = {}
|
|
668
|
+
|
|
669
|
+
path: str = "/api/v5/market/history-candles"
|
|
670
|
+
limit: str = "100"
|
|
671
|
+
|
|
672
|
+
if not req.end:
|
|
673
|
+
req.end = datetime.now()
|
|
674
|
+
|
|
675
|
+
after: str = str(int(req.end.timestamp() * 1000))
|
|
676
|
+
|
|
677
|
+
# Loop until no more data or request fails
|
|
678
|
+
while True:
|
|
679
|
+
# Create query params
|
|
680
|
+
params: dict = {
|
|
681
|
+
"instId": contract.name,
|
|
682
|
+
"bar": INTERVAL_VT2OKX[req.interval],
|
|
683
|
+
"limit": limit,
|
|
684
|
+
"after": after
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
# Get response from server
|
|
688
|
+
resp: Response = self.request(
|
|
689
|
+
"GET",
|
|
690
|
+
path,
|
|
691
|
+
params=params
|
|
692
|
+
)
|
|
693
|
+
|
|
694
|
+
# Break loop if request is failed
|
|
695
|
+
if resp.status_code // 100 != 2:
|
|
696
|
+
log_msg: str = f"Query kline history failed, status code: {resp.status_code}, message: {resp.text}"
|
|
697
|
+
self.gateway.write_log(log_msg)
|
|
698
|
+
break
|
|
699
|
+
else:
|
|
700
|
+
data: dict = resp.json()
|
|
701
|
+
bar_data: list = data.get("data", None)
|
|
702
|
+
|
|
703
|
+
if not bar_data:
|
|
704
|
+
msg: str = data["msg"]
|
|
705
|
+
log_msg = f"No kline history data received, {msg}"
|
|
706
|
+
break
|
|
707
|
+
|
|
708
|
+
for row in bar_data:
|
|
709
|
+
ts, op, hp, lp, cp, volume, turnover, _, _ = row
|
|
710
|
+
|
|
711
|
+
dt: datetime = parse_timestamp(ts)
|
|
712
|
+
|
|
713
|
+
bar: BarData = BarData(
|
|
714
|
+
symbol=req.symbol,
|
|
715
|
+
exchange=req.exchange,
|
|
716
|
+
datetime=dt,
|
|
717
|
+
interval=req.interval,
|
|
718
|
+
volume=float(volume),
|
|
719
|
+
turnover=float(turnover),
|
|
720
|
+
open_price=float(op),
|
|
721
|
+
high_price=float(hp),
|
|
722
|
+
low_price=float(lp),
|
|
723
|
+
close_price=float(cp),
|
|
724
|
+
gateway_name=self.gateway_name
|
|
725
|
+
)
|
|
726
|
+
buf[bar.datetime] = bar
|
|
727
|
+
|
|
728
|
+
begin: str = bar_data[-1][0]
|
|
729
|
+
begin_dt: datetime = parse_timestamp(begin)
|
|
730
|
+
end: str = bar_data[0][0]
|
|
731
|
+
end_dt: datetime = parse_timestamp(end)
|
|
732
|
+
|
|
733
|
+
log_msg = f"Query kline history finished, {req.symbol} - {req.interval.value}, {begin_dt} - {end_dt}"
|
|
734
|
+
self.gateway.write_log(log_msg)
|
|
735
|
+
|
|
736
|
+
# Break if all bars have been queried
|
|
737
|
+
if begin_dt <= req.start:
|
|
738
|
+
break
|
|
739
|
+
|
|
740
|
+
# Update start time
|
|
741
|
+
after = begin
|
|
742
|
+
|
|
743
|
+
index: list[datetime] = list(buf.keys())
|
|
744
|
+
index.sort()
|
|
745
|
+
|
|
746
|
+
history: list[BarData] = [buf[i] for i in index]
|
|
747
|
+
return history
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
class PublicApi(WebsocketClient):
|
|
751
|
+
"""The public websocket API of OkxGateway"""
|
|
752
|
+
|
|
753
|
+
def __init__(self, gateway: OkxGateway) -> None:
|
|
754
|
+
"""
|
|
755
|
+
The init method of the api.
|
|
756
|
+
|
|
757
|
+
Parameters:
|
|
758
|
+
gateway: the parent gateway object for pushing callback data.
|
|
759
|
+
"""
|
|
760
|
+
super().__init__()
|
|
761
|
+
|
|
762
|
+
self.gateway: OkxGateway = gateway
|
|
763
|
+
self.gateway_name: str = gateway.gateway_name
|
|
764
|
+
|
|
765
|
+
self.subscribed: dict[str, SubscribeRequest] = {}
|
|
766
|
+
self.ticks: dict[str, TickData] = {}
|
|
767
|
+
|
|
768
|
+
self.callbacks: dict[str, Callable] = {
|
|
769
|
+
"tickers": self.on_ticker,
|
|
770
|
+
"books5": self.on_depth
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
def connect(
|
|
774
|
+
self,
|
|
775
|
+
server: str,
|
|
776
|
+
proxy_host: str,
|
|
777
|
+
proxy_port: int,
|
|
778
|
+
) -> None:
|
|
779
|
+
"""
|
|
780
|
+
Start server connection.
|
|
781
|
+
|
|
782
|
+
This method establishes a websocket connection to OKX public data stream.
|
|
783
|
+
|
|
784
|
+
Parameters:
|
|
785
|
+
server: Server type ("REAL", "AWS", or "DEMO")
|
|
786
|
+
proxy_host: Proxy server hostname or IP
|
|
787
|
+
proxy_port: Proxy server port
|
|
788
|
+
"""
|
|
789
|
+
server_hosts: dict[str, str] = {
|
|
790
|
+
"REAL": REAL_PUBLIC_HOST,
|
|
791
|
+
"AWS": AWS_PUBLIC_HOST,
|
|
792
|
+
"DEMO": DEMO_PUBLIC_HOST,
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
host: str = server_hosts[server]
|
|
796
|
+
self.init(host, proxy_host, proxy_port, 20)
|
|
797
|
+
|
|
798
|
+
self.start()
|
|
799
|
+
|
|
800
|
+
def subscribe(self, req: SubscribeRequest) -> None:
|
|
801
|
+
"""
|
|
802
|
+
Subscribe to market data.
|
|
803
|
+
|
|
804
|
+
This function sends subscription requests for ticker and depth data
|
|
805
|
+
for the specified trading instrument.
|
|
806
|
+
|
|
807
|
+
Parameters:
|
|
808
|
+
req: Subscription request object containing symbol information
|
|
809
|
+
"""
|
|
810
|
+
# Get contract by VeighNa symbol
|
|
811
|
+
contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
|
|
812
|
+
if not contract:
|
|
813
|
+
self.gateway.write_log(f"Failed to subscribe data, symbol not found: {req.symbol}")
|
|
814
|
+
return
|
|
815
|
+
|
|
816
|
+
# Add subscribe record
|
|
817
|
+
self.subscribed[req.vt_symbol] = req
|
|
818
|
+
|
|
819
|
+
# Create tick object
|
|
820
|
+
tick: TickData = TickData(
|
|
821
|
+
symbol=req.symbol,
|
|
822
|
+
exchange=req.exchange,
|
|
823
|
+
name=contract.name,
|
|
824
|
+
datetime=datetime.now(CHINA_TZ),
|
|
825
|
+
gateway_name=self.gateway_name,
|
|
826
|
+
)
|
|
827
|
+
self.ticks[contract.name] = tick
|
|
828
|
+
|
|
829
|
+
# Send request to subscribe
|
|
830
|
+
args: list = []
|
|
831
|
+
for channel in ["tickers", "books5"]:
|
|
832
|
+
args.append({
|
|
833
|
+
"channel": channel,
|
|
834
|
+
"instId": contract.name
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
packet: dict = {
|
|
838
|
+
"op": "subscribe",
|
|
839
|
+
"args": args
|
|
840
|
+
}
|
|
841
|
+
self.send_packet(packet)
|
|
842
|
+
|
|
843
|
+
def on_connected(self) -> None:
|
|
844
|
+
"""
|
|
845
|
+
Callback when server is connected.
|
|
846
|
+
|
|
847
|
+
This function is called when the websocket connection to the server
|
|
848
|
+
is successfully established. It logs the connection status and
|
|
849
|
+
resubscribes to previously subscribed market data channels.
|
|
850
|
+
"""
|
|
851
|
+
self.gateway.write_log("Public API connected")
|
|
852
|
+
|
|
853
|
+
for req in list(self.subscribed.values()):
|
|
854
|
+
self.subscribe(req)
|
|
855
|
+
|
|
856
|
+
def on_disconnected(self) -> None:
|
|
857
|
+
"""
|
|
858
|
+
Callback when server is disconnected.
|
|
859
|
+
|
|
860
|
+
This function is called when the websocket connection is closed.
|
|
861
|
+
It logs the disconnection status.
|
|
862
|
+
"""
|
|
863
|
+
self.gateway.write_log("Public API disconnected")
|
|
864
|
+
|
|
865
|
+
def on_packet(self, packet: dict) -> None:
|
|
866
|
+
"""
|
|
867
|
+
Callback of data update.
|
|
868
|
+
|
|
869
|
+
This function processes different types of market data updates,
|
|
870
|
+
including ticker and depth data. It routes the data to the
|
|
871
|
+
appropriate callback function based on the channel.
|
|
872
|
+
|
|
873
|
+
Parameters:
|
|
874
|
+
packet: JSON data received from websocket
|
|
875
|
+
"""
|
|
876
|
+
if "event" in packet:
|
|
877
|
+
event: str = packet["event"]
|
|
878
|
+
if event == "subscribe":
|
|
879
|
+
return
|
|
880
|
+
elif event == "error":
|
|
881
|
+
code: str = packet["code"]
|
|
882
|
+
msg: str = packet["msg"]
|
|
883
|
+
self.gateway.write_log(f"Public API request failed, status code: {code}, message: {msg}")
|
|
884
|
+
else:
|
|
885
|
+
channel: str = packet["arg"]["channel"]
|
|
886
|
+
callback: Callable | None = self.callbacks.get(channel, None)
|
|
887
|
+
|
|
888
|
+
if callback:
|
|
889
|
+
data: list = packet["data"]
|
|
890
|
+
callback(data)
|
|
891
|
+
|
|
892
|
+
def on_error(self, exc: type, value: Exception, tb: TracebackType) -> None:
|
|
893
|
+
"""
|
|
894
|
+
General error callback.
|
|
895
|
+
|
|
896
|
+
This function is called when an exception occurs in the websocket connection.
|
|
897
|
+
It logs the exception details for troubleshooting.
|
|
898
|
+
|
|
899
|
+
Parameters:
|
|
900
|
+
exc: Type of the exception
|
|
901
|
+
value: Exception instance
|
|
902
|
+
tb: Traceback object
|
|
903
|
+
"""
|
|
904
|
+
detail: str = self.exception_detail(exc, value, tb)
|
|
905
|
+
|
|
906
|
+
msg: str = f"Exception catched by Public API: {detail}"
|
|
907
|
+
self.gateway.write_log(msg)
|
|
908
|
+
|
|
909
|
+
def on_ticker(self, data: list) -> None:
|
|
910
|
+
"""
|
|
911
|
+
Callback of ticker update.
|
|
912
|
+
|
|
913
|
+
This function processes the ticker data updates and
|
|
914
|
+
updates the corresponding TickData objects.
|
|
915
|
+
|
|
916
|
+
Parameters:
|
|
917
|
+
data: Ticker data from websocket
|
|
918
|
+
"""
|
|
919
|
+
for d in data:
|
|
920
|
+
tick: TickData = self.ticks[d["instId"]]
|
|
921
|
+
|
|
922
|
+
tick.last_price = float(d["last"])
|
|
923
|
+
tick.open_price = float(d["open24h"])
|
|
924
|
+
tick.high_price = float(d["high24h"])
|
|
925
|
+
tick.low_price = float(d["low24h"])
|
|
926
|
+
tick.volume = float(d["vol24h"])
|
|
927
|
+
tick.turnover = float(d["volCcy24h"])
|
|
928
|
+
|
|
929
|
+
tick.datetime = parse_timestamp(d["ts"])
|
|
930
|
+
self.gateway.on_tick(copy(tick))
|
|
931
|
+
|
|
932
|
+
def on_depth(self, data: list) -> None:
|
|
933
|
+
"""
|
|
934
|
+
Callback of depth update.
|
|
935
|
+
|
|
936
|
+
This function processes the order book depth data updates
|
|
937
|
+
and updates the corresponding TickData objects.
|
|
938
|
+
|
|
939
|
+
Parameters:
|
|
940
|
+
data: Depth data from websocket
|
|
941
|
+
"""
|
|
942
|
+
for d in data:
|
|
943
|
+
tick: TickData = self.ticks[d["instId"]]
|
|
944
|
+
bids: list = d["bids"]
|
|
945
|
+
asks: list = d["asks"]
|
|
946
|
+
|
|
947
|
+
for n in range(min(5, len(bids))):
|
|
948
|
+
price, volume, _, _ = bids[n]
|
|
949
|
+
tick.__setattr__("bid_price_%s" % (n + 1), float(price))
|
|
950
|
+
tick.__setattr__("bid_volume_%s" % (n + 1), float(volume))
|
|
951
|
+
|
|
952
|
+
for n in range(min(5, len(asks))):
|
|
953
|
+
price, volume, _, _ = asks[n]
|
|
954
|
+
tick.__setattr__("ask_price_%s" % (n + 1), float(price))
|
|
955
|
+
tick.__setattr__("ask_volume_%s" % (n + 1), float(volume))
|
|
956
|
+
|
|
957
|
+
tick.datetime = parse_timestamp(d["ts"])
|
|
958
|
+
self.gateway.on_tick(copy(tick))
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
class PrivateApi(WebsocketClient):
|
|
962
|
+
"""The private websocket API of OkxGateway"""
|
|
963
|
+
|
|
964
|
+
def __init__(self, gateway: OkxGateway) -> None:
|
|
965
|
+
"""
|
|
966
|
+
The init method of the api.
|
|
967
|
+
|
|
968
|
+
Parameters:
|
|
969
|
+
gateway: the parent gateway object for pushing callback data.
|
|
970
|
+
"""
|
|
971
|
+
super().__init__()
|
|
972
|
+
|
|
973
|
+
self.gateway: OkxGateway = gateway
|
|
974
|
+
self.gateway_name: str = gateway.gateway_name
|
|
975
|
+
self.local_orderids: set[str] = gateway.local_orderids
|
|
976
|
+
|
|
977
|
+
self.key: str = ""
|
|
978
|
+
self.secret: bytes = b""
|
|
979
|
+
self.passphrase: str = ""
|
|
980
|
+
|
|
981
|
+
self.reqid: int = 0
|
|
982
|
+
self.order_count: int = 0
|
|
983
|
+
self.connect_time: int = 0
|
|
984
|
+
|
|
985
|
+
self.callbacks: dict[str, Callable] = {
|
|
986
|
+
"login": self.on_login,
|
|
987
|
+
"orders": self.on_order,
|
|
988
|
+
"account": self.on_account,
|
|
989
|
+
"positions": self.on_position,
|
|
990
|
+
"order": self.on_send_order,
|
|
991
|
+
"cancel-order": self.on_cancel_order,
|
|
992
|
+
"error": self.on_api_error
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
self.reqid_order_map: dict[str, OrderData] = {}
|
|
996
|
+
|
|
997
|
+
def connect(
|
|
998
|
+
self,
|
|
999
|
+
key: str,
|
|
1000
|
+
secret: str,
|
|
1001
|
+
passphrase: str,
|
|
1002
|
+
server: str,
|
|
1003
|
+
proxy_host: str,
|
|
1004
|
+
proxy_port: int,
|
|
1005
|
+
) -> None:
|
|
1006
|
+
"""
|
|
1007
|
+
Start server connection.
|
|
1008
|
+
|
|
1009
|
+
This method establishes a websocket connection to OKX private data stream.
|
|
1010
|
+
|
|
1011
|
+
Parameters:
|
|
1012
|
+
key: API Key for authentication
|
|
1013
|
+
secret: API Secret for request signing
|
|
1014
|
+
passphrase: API Passphrase for authentication
|
|
1015
|
+
server: Server type ("REAL", "AWS", or "DEMO")
|
|
1016
|
+
proxy_host: Proxy server hostname or IP
|
|
1017
|
+
proxy_port: Proxy server port
|
|
1018
|
+
"""
|
|
1019
|
+
self.key = key
|
|
1020
|
+
self.secret = secret.encode()
|
|
1021
|
+
self.passphrase = passphrase
|
|
1022
|
+
|
|
1023
|
+
self.connect_time = int(datetime.now().strftime("%y%m%d%H%M%S"))
|
|
1024
|
+
|
|
1025
|
+
server_hosts: dict[str, str] = {
|
|
1026
|
+
"REAL": REAL_PRIVATE_HOST,
|
|
1027
|
+
"AWS": AWS_PRIVATE_HOST,
|
|
1028
|
+
"DEMO": DEMO_PRIVATE_HOST,
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
host: str = server_hosts[server]
|
|
1032
|
+
self.init(host, proxy_host, proxy_port, 20)
|
|
1033
|
+
|
|
1034
|
+
self.start()
|
|
1035
|
+
|
|
1036
|
+
def on_connected(self) -> None:
|
|
1037
|
+
"""
|
|
1038
|
+
Callback when server is connected.
|
|
1039
|
+
|
|
1040
|
+
This function is called when the websocket connection to the server
|
|
1041
|
+
is successfully established. It logs the connection status and
|
|
1042
|
+
initiates the login process.
|
|
1043
|
+
"""
|
|
1044
|
+
self.gateway.write_log("Private websocket API connected")
|
|
1045
|
+
self.login()
|
|
1046
|
+
|
|
1047
|
+
def on_disconnected(self) -> None:
|
|
1048
|
+
"""
|
|
1049
|
+
Callback when server is disconnected.
|
|
1050
|
+
|
|
1051
|
+
This function is called when the websocket connection is closed.
|
|
1052
|
+
It logs the disconnection status.
|
|
1053
|
+
"""
|
|
1054
|
+
self.gateway.write_log("Private API disconnected")
|
|
1055
|
+
|
|
1056
|
+
def on_packet(self, packet: dict) -> None:
|
|
1057
|
+
"""
|
|
1058
|
+
Callback of data update.
|
|
1059
|
+
|
|
1060
|
+
This function processes different types of private data updates,
|
|
1061
|
+
including orders, account balance, and positions. It routes the data
|
|
1062
|
+
to the appropriate callback function.
|
|
1063
|
+
|
|
1064
|
+
Parameters:
|
|
1065
|
+
packet: JSON data received from websocket
|
|
1066
|
+
"""
|
|
1067
|
+
if "event" in packet:
|
|
1068
|
+
cb_name: str = packet["event"]
|
|
1069
|
+
elif "op" in packet:
|
|
1070
|
+
cb_name = packet["op"]
|
|
1071
|
+
else:
|
|
1072
|
+
cb_name = packet["arg"]["channel"]
|
|
1073
|
+
|
|
1074
|
+
callback: Callable | None = self.callbacks.get(cb_name, None)
|
|
1075
|
+
if callback:
|
|
1076
|
+
callback(packet)
|
|
1077
|
+
|
|
1078
|
+
def on_error(self, e: Exception) -> None:
|
|
1079
|
+
"""
|
|
1080
|
+
General error callback.
|
|
1081
|
+
|
|
1082
|
+
This function is called when an exception occurs in the websocket connection.
|
|
1083
|
+
It logs the exception details for troubleshooting.
|
|
1084
|
+
|
|
1085
|
+
Parameters:
|
|
1086
|
+
e: The exception that was raised
|
|
1087
|
+
"""
|
|
1088
|
+
msg: str = f"Private channel exception triggered: {e}"
|
|
1089
|
+
self.gateway.write_log(msg)
|
|
1090
|
+
|
|
1091
|
+
def on_api_error(self, packet: dict) -> None:
|
|
1092
|
+
"""
|
|
1093
|
+
Callback of API error.
|
|
1094
|
+
|
|
1095
|
+
This function processes error responses from the websocket API.
|
|
1096
|
+
It logs the error details for troubleshooting.
|
|
1097
|
+
|
|
1098
|
+
Parameters:
|
|
1099
|
+
packet: Error data from websocket
|
|
1100
|
+
"""
|
|
1101
|
+
# Extract error code and message from the response
|
|
1102
|
+
code: str = packet["code"]
|
|
1103
|
+
msg: str = packet["msg"]
|
|
1104
|
+
|
|
1105
|
+
# Log the error with details for debugging
|
|
1106
|
+
self.gateway.write_log(f"Private API request failed, status code: {code}, message: {msg}")
|
|
1107
|
+
|
|
1108
|
+
def on_login(self, packet: dict) -> None:
|
|
1109
|
+
"""
|
|
1110
|
+
Callback of user login.
|
|
1111
|
+
|
|
1112
|
+
This function processes the login response and subscribes to
|
|
1113
|
+
private data channels if login is successful.
|
|
1114
|
+
|
|
1115
|
+
Parameters:
|
|
1116
|
+
packet: Login response data from websocket
|
|
1117
|
+
"""
|
|
1118
|
+
if packet["code"] == '0':
|
|
1119
|
+
self.gateway.write_log("Private API login successful")
|
|
1120
|
+
self.subscribe_topic()
|
|
1121
|
+
else:
|
|
1122
|
+
self.gateway.write_log("Private API login failed")
|
|
1123
|
+
|
|
1124
|
+
def on_order(self, packet: dict) -> None:
|
|
1125
|
+
"""
|
|
1126
|
+
Callback of order update.
|
|
1127
|
+
|
|
1128
|
+
This function processes order updates and trade executions.
|
|
1129
|
+
It creates OrderData and TradeData objects and pushes them to the gateway.
|
|
1130
|
+
|
|
1131
|
+
Parameters:
|
|
1132
|
+
packet: Order update data from websocket
|
|
1133
|
+
"""
|
|
1134
|
+
# Extract order data from packet
|
|
1135
|
+
data: list = packet["data"]
|
|
1136
|
+
for d in data:
|
|
1137
|
+
# Create order object from data
|
|
1138
|
+
order: OrderData = self.gateway.parse_order_data(d, self.gateway_name)
|
|
1139
|
+
self.gateway.on_order(order)
|
|
1140
|
+
|
|
1141
|
+
# Check if order is filled - skip trade creation if no fill size
|
|
1142
|
+
if d["fillSz"] == "0":
|
|
1143
|
+
return
|
|
1144
|
+
|
|
1145
|
+
# Process trade data for filled or partially filled orders
|
|
1146
|
+
# Round trade volume number to meet minimum volume precision
|
|
1147
|
+
trade_volume: float = float(d["fillSz"])
|
|
1148
|
+
contract: ContractData = self.gateway.get_contract_by_symbol(order.symbol)
|
|
1149
|
+
if contract:
|
|
1150
|
+
trade_volume = round_to(trade_volume, contract.min_volume)
|
|
1151
|
+
|
|
1152
|
+
# Create trade object and push to gateway
|
|
1153
|
+
trade: TradeData = TradeData(
|
|
1154
|
+
symbol=order.symbol,
|
|
1155
|
+
exchange=order.exchange,
|
|
1156
|
+
orderid=order.orderid,
|
|
1157
|
+
tradeid=d["tradeId"],
|
|
1158
|
+
direction=order.direction,
|
|
1159
|
+
offset=order.offset,
|
|
1160
|
+
price=float(d["fillPx"]),
|
|
1161
|
+
volume=trade_volume,
|
|
1162
|
+
datetime=parse_timestamp(d["uTime"]),
|
|
1163
|
+
gateway_name=self.gateway_name,
|
|
1164
|
+
)
|
|
1165
|
+
self.gateway.on_trade(trade)
|
|
1166
|
+
|
|
1167
|
+
def on_account(self, packet: dict) -> None:
|
|
1168
|
+
"""
|
|
1169
|
+
Callback of account balance update.
|
|
1170
|
+
|
|
1171
|
+
This function processes account balance updates and creates
|
|
1172
|
+
AccountData objects for each asset.
|
|
1173
|
+
|
|
1174
|
+
Parameters:
|
|
1175
|
+
packet: Account update data from websocket
|
|
1176
|
+
"""
|
|
1177
|
+
if len(packet["data"]) == 0:
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
buf: dict = packet["data"][0]
|
|
1181
|
+
for detail in buf["details"]:
|
|
1182
|
+
account: AccountData = AccountData(
|
|
1183
|
+
accountid=detail["ccy"],
|
|
1184
|
+
balance=float(detail["eq"]),
|
|
1185
|
+
gateway_name=self.gateway_name,
|
|
1186
|
+
)
|
|
1187
|
+
account.available = float(detail["availEq"]) if len(detail["availEq"]) != 0 else 0.0
|
|
1188
|
+
account.frozen = account.balance - account.available
|
|
1189
|
+
self.gateway.on_account(account)
|
|
1190
|
+
|
|
1191
|
+
def on_position(self, packet: dict) -> None:
|
|
1192
|
+
"""
|
|
1193
|
+
Callback of position update.
|
|
1194
|
+
|
|
1195
|
+
This function processes position updates and creates
|
|
1196
|
+
PositionData objects for each position.
|
|
1197
|
+
|
|
1198
|
+
Parameters:
|
|
1199
|
+
packet: Position update data from websocket
|
|
1200
|
+
"""
|
|
1201
|
+
data: list = packet["data"]
|
|
1202
|
+
for d in data:
|
|
1203
|
+
name: str = d["instId"]
|
|
1204
|
+
contract: ContractData = self.gateway.get_contract_by_name(name)
|
|
1205
|
+
|
|
1206
|
+
pos: float = float(d["pos"])
|
|
1207
|
+
price: float = get_float_value(d, "avgPx")
|
|
1208
|
+
pnl: float = get_float_value(d, "upl")
|
|
1209
|
+
|
|
1210
|
+
position: PositionData = PositionData(
|
|
1211
|
+
symbol=contract.symbol,
|
|
1212
|
+
exchange=Exchange.GLOBAL,
|
|
1213
|
+
direction=Direction.NET,
|
|
1214
|
+
volume=pos,
|
|
1215
|
+
price=price,
|
|
1216
|
+
pnl=pnl,
|
|
1217
|
+
gateway_name=self.gateway_name,
|
|
1218
|
+
)
|
|
1219
|
+
self.gateway.on_position(position)
|
|
1220
|
+
|
|
1221
|
+
def on_send_order(self, packet: dict) -> None:
|
|
1222
|
+
"""
|
|
1223
|
+
Callback of send_order.
|
|
1224
|
+
|
|
1225
|
+
This function processes the response to an order placement request.
|
|
1226
|
+
It handles errors and rejection cases.
|
|
1227
|
+
|
|
1228
|
+
Parameters:
|
|
1229
|
+
packet: Order response data from websocket
|
|
1230
|
+
"""
|
|
1231
|
+
data: list = packet["data"]
|
|
1232
|
+
|
|
1233
|
+
# Wrong parameters
|
|
1234
|
+
if packet["code"] != "0":
|
|
1235
|
+
if not data:
|
|
1236
|
+
order: OrderData = self.reqid_order_map[packet["id"]]
|
|
1237
|
+
order.status = Status.REJECTED
|
|
1238
|
+
self.gateway.on_order(order)
|
|
1239
|
+
return
|
|
1240
|
+
|
|
1241
|
+
# Failed to process
|
|
1242
|
+
for d in data:
|
|
1243
|
+
code: str = d["sCode"]
|
|
1244
|
+
if code == "0":
|
|
1245
|
+
return
|
|
1246
|
+
|
|
1247
|
+
orderid: str = d["clOrdId"]
|
|
1248
|
+
order = self.gateway.get_order(orderid)
|
|
1249
|
+
if not order:
|
|
1250
|
+
return
|
|
1251
|
+
order.status = Status.REJECTED
|
|
1252
|
+
self.gateway.on_order(copy(order))
|
|
1253
|
+
|
|
1254
|
+
msg: str = d["sMsg"]
|
|
1255
|
+
self.gateway.write_log(f"Send order failed, status code: {code}, message: {msg}")
|
|
1256
|
+
|
|
1257
|
+
def on_cancel_order(self, packet: dict) -> None:
|
|
1258
|
+
"""
|
|
1259
|
+
Callback of cancel_order.
|
|
1260
|
+
|
|
1261
|
+
This function processes the response to an order cancellation request.
|
|
1262
|
+
It handles errors and logs appropriate messages.
|
|
1263
|
+
|
|
1264
|
+
Parameters:
|
|
1265
|
+
packet: Cancel response data from websocket
|
|
1266
|
+
"""
|
|
1267
|
+
# Wrong parameters
|
|
1268
|
+
if packet["code"] != "0":
|
|
1269
|
+
code: str = packet["code"]
|
|
1270
|
+
msg: str = packet["msg"]
|
|
1271
|
+
self.gateway.write_log(f"Cancel order failed, status code: {code}, message: {msg}")
|
|
1272
|
+
return
|
|
1273
|
+
|
|
1274
|
+
# Failed to process
|
|
1275
|
+
data: list = packet["data"]
|
|
1276
|
+
for d in data:
|
|
1277
|
+
code = d["sCode"]
|
|
1278
|
+
if code == "0":
|
|
1279
|
+
return
|
|
1280
|
+
|
|
1281
|
+
msg = d["sMsg"]
|
|
1282
|
+
self.gateway.write_log(f"Cancel order failed, status code: {code}, message: {msg}")
|
|
1283
|
+
|
|
1284
|
+
def login(self) -> None:
|
|
1285
|
+
"""
|
|
1286
|
+
User login.
|
|
1287
|
+
|
|
1288
|
+
This function prepares and sends a login request to authenticate
|
|
1289
|
+
with the websocket API using API credentials.
|
|
1290
|
+
"""
|
|
1291
|
+
timestamp: str = str(time.time())
|
|
1292
|
+
msg: str = timestamp + "GET" + "/users/self/verify"
|
|
1293
|
+
signature: bytes = generate_signature(msg, self.secret)
|
|
1294
|
+
|
|
1295
|
+
packet: dict = {
|
|
1296
|
+
"op": "login",
|
|
1297
|
+
"args":
|
|
1298
|
+
[
|
|
1299
|
+
{
|
|
1300
|
+
"apiKey": self.key,
|
|
1301
|
+
"passphrase": self.passphrase,
|
|
1302
|
+
"timestamp": timestamp,
|
|
1303
|
+
"sign": signature.decode("utf-8")
|
|
1304
|
+
}
|
|
1305
|
+
]
|
|
1306
|
+
}
|
|
1307
|
+
self.send_packet(packet)
|
|
1308
|
+
|
|
1309
|
+
def subscribe_topic(self) -> None:
|
|
1310
|
+
"""
|
|
1311
|
+
Subscribe to private data channels.
|
|
1312
|
+
|
|
1313
|
+
This function sends subscription requests for order, account, and
|
|
1314
|
+
position updates after successful login.
|
|
1315
|
+
"""
|
|
1316
|
+
packet: dict = {
|
|
1317
|
+
"op": "subscribe",
|
|
1318
|
+
"args": [
|
|
1319
|
+
{
|
|
1320
|
+
"channel": "orders",
|
|
1321
|
+
"instType": "ANY"
|
|
1322
|
+
},
|
|
1323
|
+
{
|
|
1324
|
+
"channel": "account"
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
"channel": "positions",
|
|
1328
|
+
"instType": "ANY"
|
|
1329
|
+
},
|
|
1330
|
+
]
|
|
1331
|
+
}
|
|
1332
|
+
self.send_packet(packet)
|
|
1333
|
+
|
|
1334
|
+
def send_order(self, req: OrderRequest) -> str:
|
|
1335
|
+
"""
|
|
1336
|
+
Send new order to OKX.
|
|
1337
|
+
|
|
1338
|
+
This function creates and sends a new order request to the exchange.
|
|
1339
|
+
It handles different order types and trading modes.
|
|
1340
|
+
|
|
1341
|
+
Parameters:
|
|
1342
|
+
req: Order request object containing order details
|
|
1343
|
+
|
|
1344
|
+
Returns:
|
|
1345
|
+
str: The VeighNa order ID if successful, empty string otherwise
|
|
1346
|
+
"""
|
|
1347
|
+
# Validate order type is supported by OKX
|
|
1348
|
+
if req.type not in ORDERTYPE_VT2OKX:
|
|
1349
|
+
self.gateway.write_log(f"Send order failed, order type not supported: {req.type.value}")
|
|
1350
|
+
return ""
|
|
1351
|
+
|
|
1352
|
+
# Validate symbol exists in contract map
|
|
1353
|
+
contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
|
|
1354
|
+
if not contract:
|
|
1355
|
+
self.gateway.write_log(f"Send order failed, symbol not found: {req.symbol}")
|
|
1356
|
+
return ""
|
|
1357
|
+
|
|
1358
|
+
# Generate unique local order ID
|
|
1359
|
+
self.order_count += 1
|
|
1360
|
+
count_str = str(self.order_count).rjust(6, "0")
|
|
1361
|
+
orderid = f"{self.connect_time}{count_str}"
|
|
1362
|
+
|
|
1363
|
+
# Prepare order parameters for OKX API
|
|
1364
|
+
args: dict = {
|
|
1365
|
+
"instId": contract.name,
|
|
1366
|
+
"clOrdId": orderid,
|
|
1367
|
+
"side": DIRECTION_VT2OKX[req.direction],
|
|
1368
|
+
"ordType": ORDERTYPE_VT2OKX[req.type],
|
|
1369
|
+
"px": str(req.price),
|
|
1370
|
+
"sz": str(req.volume)
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
# Set trading mode based on product type
|
|
1374
|
+
# "cash" for spot trading, "cross" for futures/swap with cross margin
|
|
1375
|
+
if contract.product == Product.SPOT:
|
|
1376
|
+
args["tdMode"] = "cash"
|
|
1377
|
+
else:
|
|
1378
|
+
args["tdMode"] = "cross"
|
|
1379
|
+
|
|
1380
|
+
# Create websocket request with unique request ID
|
|
1381
|
+
self.reqid += 1
|
|
1382
|
+
packet: dict = {
|
|
1383
|
+
"id": str(self.reqid),
|
|
1384
|
+
"op": "order",
|
|
1385
|
+
"args": [args]
|
|
1386
|
+
}
|
|
1387
|
+
self.send_packet(packet)
|
|
1388
|
+
|
|
1389
|
+
# Create order data object and push to gateway
|
|
1390
|
+
order: OrderData = req.create_order_data(orderid, self.gateway_name)
|
|
1391
|
+
self.gateway.on_order(order)
|
|
1392
|
+
|
|
1393
|
+
# Return VeighNa order ID (gateway_name.orderid)
|
|
1394
|
+
return str(order.vt_orderid)
|
|
1395
|
+
|
|
1396
|
+
def cancel_order(self, req: CancelRequest) -> None:
|
|
1397
|
+
"""
|
|
1398
|
+
Cancel existing order on OKX.
|
|
1399
|
+
|
|
1400
|
+
This function sends a request to cancel an existing order on the exchange.
|
|
1401
|
+
It determines whether to use client order ID or exchange order ID.
|
|
1402
|
+
|
|
1403
|
+
Parameters:
|
|
1404
|
+
req: Cancel request object containing order details
|
|
1405
|
+
"""
|
|
1406
|
+
# Validate symbol exists in contract map
|
|
1407
|
+
contract: ContractData | None = self.gateway.get_contract_by_symbol(req.symbol)
|
|
1408
|
+
if not contract:
|
|
1409
|
+
self.gateway.write_log(f"Cancel order failed, symbol not found: {req.symbol}")
|
|
1410
|
+
return
|
|
1411
|
+
|
|
1412
|
+
# Initialize cancel parameters with instrument ID
|
|
1413
|
+
args: dict = {"instId": contract.name}
|
|
1414
|
+
|
|
1415
|
+
# Determine the type of order ID to use for cancellation
|
|
1416
|
+
# OKX supports both client order ID and exchange order ID for cancellation
|
|
1417
|
+
if req.orderid in self.local_orderids:
|
|
1418
|
+
# Use client order ID if it was created by this gateway instance
|
|
1419
|
+
args["clOrdId"] = req.orderid
|
|
1420
|
+
else:
|
|
1421
|
+
# Use exchange order ID if it came from another source
|
|
1422
|
+
args["ordId"] = req.orderid
|
|
1423
|
+
|
|
1424
|
+
# Create websocket request with unique request ID
|
|
1425
|
+
self.reqid += 1
|
|
1426
|
+
packet: dict = {
|
|
1427
|
+
"id": str(self.reqid),
|
|
1428
|
+
"op": "cancel-order",
|
|
1429
|
+
"args": [args]
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
# Send the cancellation request
|
|
1433
|
+
self.send_packet(packet)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def generate_signature(msg: str, secret_key: bytes) -> bytes:
|
|
1437
|
+
"""
|
|
1438
|
+
Generate signature from message.
|
|
1439
|
+
|
|
1440
|
+
This function creates an HMAC-SHA256 signature required for
|
|
1441
|
+
authenticated API requests to OKX.
|
|
1442
|
+
|
|
1443
|
+
Parameters:
|
|
1444
|
+
msg: Message to be signed
|
|
1445
|
+
secret_key: API secret key in bytes
|
|
1446
|
+
|
|
1447
|
+
Returns:
|
|
1448
|
+
bytes: Base64 encoded signature
|
|
1449
|
+
"""
|
|
1450
|
+
return base64.b64encode(hmac.new(secret_key, msg.encode(), hashlib.sha256).digest())
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def generate_timestamp() -> str:
|
|
1454
|
+
"""
|
|
1455
|
+
Generate current timestamp.
|
|
1456
|
+
|
|
1457
|
+
This function creates an ISO format timestamp with milliseconds
|
|
1458
|
+
required for OKX API requests.
|
|
1459
|
+
|
|
1460
|
+
Returns:
|
|
1461
|
+
str: ISO 8601 formatted timestamp with Z suffix
|
|
1462
|
+
"""
|
|
1463
|
+
now: datetime = datetime.utcnow()
|
|
1464
|
+
timestamp: str = now.isoformat("T", "milliseconds")
|
|
1465
|
+
return timestamp + "Z"
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def parse_timestamp(timestamp: str) -> datetime:
|
|
1469
|
+
"""
|
|
1470
|
+
Parse timestamp to datetime.
|
|
1471
|
+
|
|
1472
|
+
This function converts OKX timestamp to a datetime object
|
|
1473
|
+
with UTC timezone.
|
|
1474
|
+
|
|
1475
|
+
Parameters:
|
|
1476
|
+
timestamp: OKX timestamp in milliseconds
|
|
1477
|
+
|
|
1478
|
+
Returns:
|
|
1479
|
+
datetime: Datetime object with UTC timezone
|
|
1480
|
+
"""
|
|
1481
|
+
dt: datetime = datetime.fromtimestamp(int(timestamp) / 1000)
|
|
1482
|
+
return dt.replace(tzinfo=CHINA_TZ)
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def get_float_value(data: dict, key: str) -> float:
|
|
1486
|
+
"""
|
|
1487
|
+
Get decimal number from float value.
|
|
1488
|
+
|
|
1489
|
+
This function safely extracts a float value from a dictionary
|
|
1490
|
+
and handles empty or missing values.
|
|
1491
|
+
|
|
1492
|
+
Parameters:
|
|
1493
|
+
data: Dictionary containing the value
|
|
1494
|
+
key: Key to extract from the dictionary
|
|
1495
|
+
|
|
1496
|
+
Returns:
|
|
1497
|
+
float: Extracted value or 0.0 if not found
|
|
1498
|
+
"""
|
|
1499
|
+
data_str: str = data.get(key, "")
|
|
1500
|
+
if not data_str:
|
|
1501
|
+
return 0.0
|
|
1502
|
+
return float(data_str)
|