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.
@@ -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)