walrasquant-lib 0.4.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. walrasquant/__init__.py +7 -0
  2. walrasquant/aggregation.py +449 -0
  3. walrasquant/backends/__init__.py +5 -0
  4. walrasquant/backends/db.py +109 -0
  5. walrasquant/backends/db_memory.py +61 -0
  6. walrasquant/backends/db_postgresql.py +321 -0
  7. walrasquant/backends/db_sqlite.py +310 -0
  8. walrasquant/base/__init__.py +24 -0
  9. walrasquant/base/api_client.py +46 -0
  10. walrasquant/base/connector.py +863 -0
  11. walrasquant/base/ems.py +794 -0
  12. walrasquant/base/exchange.py +213 -0
  13. walrasquant/base/oms.py +428 -0
  14. walrasquant/base/retry.py +220 -0
  15. walrasquant/base/sms.py +545 -0
  16. walrasquant/base/ws_client.py +408 -0
  17. walrasquant/config.py +284 -0
  18. walrasquant/constants.py +413 -0
  19. walrasquant/core/__init__.py +0 -0
  20. walrasquant/core/cache.py +688 -0
  21. walrasquant/core/clock.py +59 -0
  22. walrasquant/core/connection.py +41 -0
  23. walrasquant/core/entity.py +504 -0
  24. walrasquant/core/nautilius_core.py +103 -0
  25. walrasquant/core/registry.py +41 -0
  26. walrasquant/engine.py +745 -0
  27. walrasquant/error.py +34 -0
  28. walrasquant/exchange/__init__.py +13 -0
  29. walrasquant/exchange/base_factory.py +172 -0
  30. walrasquant/exchange/binance/__init__.py +30 -0
  31. walrasquant/exchange/binance/connector.py +1093 -0
  32. walrasquant/exchange/binance/constants.py +934 -0
  33. walrasquant/exchange/binance/ems.py +140 -0
  34. walrasquant/exchange/binance/error.py +48 -0
  35. walrasquant/exchange/binance/exchange.py +144 -0
  36. walrasquant/exchange/binance/factory.py +115 -0
  37. walrasquant/exchange/binance/oms.py +1807 -0
  38. walrasquant/exchange/binance/rest_api.py +1653 -0
  39. walrasquant/exchange/binance/schema.py +1063 -0
  40. walrasquant/exchange/binance/websockets.py +389 -0
  41. walrasquant/exchange/bitget/__init__.py +28 -0
  42. walrasquant/exchange/bitget/connector.py +578 -0
  43. walrasquant/exchange/bitget/constants.py +392 -0
  44. walrasquant/exchange/bitget/ems.py +202 -0
  45. walrasquant/exchange/bitget/error.py +36 -0
  46. walrasquant/exchange/bitget/exchange.py +128 -0
  47. walrasquant/exchange/bitget/factory.py +135 -0
  48. walrasquant/exchange/bitget/oms.py +1619 -0
  49. walrasquant/exchange/bitget/rest_api.py +610 -0
  50. walrasquant/exchange/bitget/schema.py +885 -0
  51. walrasquant/exchange/bitget/websockets.py +753 -0
  52. walrasquant/exchange/bybit/__init__.py +32 -0
  53. walrasquant/exchange/bybit/connector.py +819 -0
  54. walrasquant/exchange/bybit/constants.py +479 -0
  55. walrasquant/exchange/bybit/ems.py +93 -0
  56. walrasquant/exchange/bybit/error.py +36 -0
  57. walrasquant/exchange/bybit/exchange.py +108 -0
  58. walrasquant/exchange/bybit/factory.py +128 -0
  59. walrasquant/exchange/bybit/oms.py +1195 -0
  60. walrasquant/exchange/bybit/rest_api.py +570 -0
  61. walrasquant/exchange/bybit/schema.py +867 -0
  62. walrasquant/exchange/bybit/websockets.py +307 -0
  63. walrasquant/exchange/hyperliquid/__init__.py +28 -0
  64. walrasquant/exchange/hyperliquid/connector.py +370 -0
  65. walrasquant/exchange/hyperliquid/constants.py +371 -0
  66. walrasquant/exchange/hyperliquid/ems.py +156 -0
  67. walrasquant/exchange/hyperliquid/error.py +48 -0
  68. walrasquant/exchange/hyperliquid/exchange.py +120 -0
  69. walrasquant/exchange/hyperliquid/factory.py +135 -0
  70. walrasquant/exchange/hyperliquid/oms.py +1081 -0
  71. walrasquant/exchange/hyperliquid/rest_api.py +348 -0
  72. walrasquant/exchange/hyperliquid/schema.py +583 -0
  73. walrasquant/exchange/hyperliquid/websockets.py +592 -0
  74. walrasquant/exchange/okx/__init__.py +25 -0
  75. walrasquant/exchange/okx/connector.py +931 -0
  76. walrasquant/exchange/okx/constants.py +518 -0
  77. walrasquant/exchange/okx/ems.py +144 -0
  78. walrasquant/exchange/okx/error.py +66 -0
  79. walrasquant/exchange/okx/exchange.py +102 -0
  80. walrasquant/exchange/okx/factory.py +138 -0
  81. walrasquant/exchange/okx/oms.py +1199 -0
  82. walrasquant/exchange/okx/rest_api.py +799 -0
  83. walrasquant/exchange/okx/schema.py +1449 -0
  84. walrasquant/exchange/okx/websockets.py +420 -0
  85. walrasquant/exchange/registry.py +201 -0
  86. walrasquant/execution/__init__.py +24 -0
  87. walrasquant/execution/algorithm.py +968 -0
  88. walrasquant/execution/algorithms/__init__.py +3 -0
  89. walrasquant/execution/algorithms/twap.py +392 -0
  90. walrasquant/execution/config.py +34 -0
  91. walrasquant/execution/constants.py +27 -0
  92. walrasquant/execution/schema.py +62 -0
  93. walrasquant/indicator.py +382 -0
  94. walrasquant/push.py +77 -0
  95. walrasquant/schema.py +755 -0
  96. walrasquant/strategy.py +1805 -0
  97. walrasquant/tools/__init__.py +0 -0
  98. walrasquant/tools/pm2_wrapper.py +1016 -0
  99. walrasquant/web/__init__.py +26 -0
  100. walrasquant/web/app.py +157 -0
  101. walrasquant/web/server.py +92 -0
  102. walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
  103. walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
  104. walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
  105. walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,799 @@
1
+ import asyncio
2
+ import msgspec
3
+ from typing import Dict, Any, Optional, Union, List, cast
4
+ import base64
5
+ from urllib.parse import urlencode
6
+ import aiohttp
7
+ from walrasquant.base import ApiClient, RetryManager
8
+ from walrasquant.exchange.okx.constants import (
9
+ OkxRateLimiter,
10
+ )
11
+ from walrasquant.exchange.okx.error import OkxHttpError, OkxRequestError, retry_check
12
+ from walrasquant.exchange.okx.schema import (
13
+ OkxPlaceOrderResponse,
14
+ OkxCancelOrderResponse,
15
+ OkxGeneralResponse,
16
+ OkxErrorResponse,
17
+ OkxBalanceResponse,
18
+ OkxPositionResponse,
19
+ OkxCandlesticksResponse,
20
+ OkxSavingsBalanceResponse,
21
+ OkxSavingsPurchaseRedemptResponse,
22
+ OkxSavingsLendingRateSummaryResponse,
23
+ OkxSavingsLendingRateHistoryResponse,
24
+ OkxAssetTransferResponse,
25
+ OkxAmendOrderResponse,
26
+ OkxFinanceStakingDefiRedeemResponse,
27
+ OkxFinanceStakingDefiPurchaseResponse,
28
+ OkxFinanceStakingDefiOffersResponse,
29
+ OkxAccountConfigResponse,
30
+ OkxIndexCandlesticksResponse,
31
+ OkxBatchOrderResponse,
32
+ OkxCancelBatchOrderResponse,
33
+ OkxTickersResponse,
34
+ OkxOrderResponse,
35
+ )
36
+ from walrasquant.core.nautilius_core import hmac_signature, LiveClock
37
+
38
+
39
+ class OkxApiClient(ApiClient):
40
+ _limiter: OkxRateLimiter
41
+
42
+ def __init__(
43
+ self,
44
+ clock: LiveClock,
45
+ api_key: Optional[str] = None,
46
+ secret: Optional[str] = None,
47
+ passphrase: Optional[str] = None,
48
+ testnet: bool = False,
49
+ timeout: int = 10,
50
+ enable_rate_limit: bool = True,
51
+ max_retries: int = 0,
52
+ delay_initial_ms: int = 100,
53
+ delay_max_ms: int = 800,
54
+ backoff_factor: int = 2,
55
+ ):
56
+ super().__init__(
57
+ clock=clock,
58
+ api_key=api_key,
59
+ secret=secret,
60
+ timeout=timeout,
61
+ rate_limiter=OkxRateLimiter(enable_rate_limit),
62
+ retry_manager=RetryManager(
63
+ max_retries=max_retries,
64
+ delay_initial_ms=delay_initial_ms,
65
+ delay_max_ms=delay_max_ms,
66
+ backoff_factor=backoff_factor,
67
+ exc_types=(OkxRequestError, aiohttp.ClientError),
68
+ retry_check=retry_check,
69
+ ),
70
+ )
71
+
72
+ self._base_url = "https://www.okx.com"
73
+ self._passphrase = passphrase
74
+ self._testnet = testnet
75
+ self._place_order_decoder = msgspec.json.Decoder(OkxPlaceOrderResponse)
76
+ self._cancel_order_decoder = msgspec.json.Decoder(OkxCancelOrderResponse)
77
+ self._general_response_decoder = msgspec.json.Decoder(OkxGeneralResponse)
78
+ self._error_response_decoder = msgspec.json.Decoder(OkxErrorResponse)
79
+ self._balance_response_decoder = msgspec.json.Decoder(
80
+ OkxBalanceResponse, strict=False
81
+ )
82
+ self._position_response_decoder = msgspec.json.Decoder(
83
+ OkxPositionResponse, strict=False
84
+ )
85
+ self._candles_response_decoder = msgspec.json.Decoder(
86
+ OkxCandlesticksResponse, strict=False
87
+ )
88
+ self._index_candles_response_decoder = msgspec.json.Decoder(
89
+ OkxIndexCandlesticksResponse, strict=False
90
+ )
91
+ self._savings_balance_response_decoder = msgspec.json.Decoder(
92
+ OkxSavingsBalanceResponse, strict=False
93
+ )
94
+
95
+ self._savings_purchase_redempt_response_decoder = msgspec.json.Decoder(
96
+ OkxSavingsPurchaseRedemptResponse, strict=False
97
+ )
98
+
99
+ self._savings_lending_rate_summary_response_decoder = msgspec.json.Decoder(
100
+ OkxSavingsLendingRateSummaryResponse, strict=False
101
+ )
102
+
103
+ self._savings_lending_rate_history_response_decoder = msgspec.json.Decoder(
104
+ OkxSavingsLendingRateHistoryResponse, strict=False
105
+ )
106
+
107
+ self._asset_transfer_response_decoder = msgspec.json.Decoder(
108
+ OkxAssetTransferResponse, strict=False
109
+ )
110
+
111
+ self._amend_order_response_decoder = msgspec.json.Decoder(
112
+ OkxAmendOrderResponse, strict=False
113
+ )
114
+
115
+ self._finance_staking_defi_redeem_response_decoder = msgspec.json.Decoder(
116
+ OkxFinanceStakingDefiRedeemResponse, strict=False
117
+ )
118
+
119
+ self._finance_staking_defi_purchase_response_decoder = msgspec.json.Decoder(
120
+ OkxFinanceStakingDefiPurchaseResponse, strict=False
121
+ )
122
+
123
+ self._finance_staking_defi_offers_response_decoder = msgspec.json.Decoder(
124
+ OkxFinanceStakingDefiOffersResponse, strict=False
125
+ )
126
+
127
+ self._account_config_response_decoder = msgspec.json.Decoder(
128
+ OkxAccountConfigResponse, strict=False
129
+ )
130
+
131
+ self._batch_order_response_decoder = msgspec.json.Decoder(OkxBatchOrderResponse)
132
+
133
+ self._cancel_batch_order_response_decoder = msgspec.json.Decoder(
134
+ OkxCancelBatchOrderResponse
135
+ )
136
+
137
+ self._tickers_response_decoder = msgspec.json.Decoder(OkxTickersResponse)
138
+
139
+ self._order_response_decoder = msgspec.json.Decoder(
140
+ OkxOrderResponse, strict=False
141
+ )
142
+
143
+ self._headers = {
144
+ "Content-Type": "application/json",
145
+ "User-Agent": "TradingBot/1.0",
146
+ }
147
+
148
+ if self._testnet:
149
+ self._headers["x-simulated-trading"] = "1"
150
+
151
+ async def get_api_v5_account_balance(
152
+ self, ccy: str | None = None
153
+ ) -> OkxBalanceResponse:
154
+ """
155
+ https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-balance
156
+ """
157
+ endpoint = "/api/v5/account/balance"
158
+ payload = {"ccy": ccy} if ccy else {}
159
+ cost = self._get_rate_limit_cost(1)
160
+ await self._limiter.query_limit(endpoint, cost=cost)
161
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=True)
162
+ return self._balance_response_decoder.decode(raw)
163
+
164
+ async def get_api_v5_account_positions(
165
+ self,
166
+ inst_type: str | None = None,
167
+ inst_id: str | None = None,
168
+ pos_id: str | None = None,
169
+ ) -> OkxPositionResponse:
170
+ """
171
+ https://www.okx.com/docs-v5/en/#trading-account-rest-api-get-positions
172
+ """
173
+ endpoint = "/api/v5/account/positions"
174
+ payload = {
175
+ k: v
176
+ for k, v in {
177
+ "instType": inst_type,
178
+ "instId": inst_id,
179
+ "posId": pos_id,
180
+ }.items()
181
+ if v is not None
182
+ }
183
+ cost = self._get_rate_limit_cost(1)
184
+ await self._limiter.query_limit(endpoint, cost=cost)
185
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=True)
186
+ return self._position_response_decoder.decode(raw)
187
+
188
+ async def post_api_v5_trade_order(
189
+ self,
190
+ inst_id: str,
191
+ td_mode: str,
192
+ side: str,
193
+ ord_type: str,
194
+ sz: str,
195
+ **kwargs,
196
+ ) -> OkxPlaceOrderResponse:
197
+ """
198
+ Place a new order
199
+ https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-place-order
200
+
201
+ {'arg': {'channel': 'orders', 'instType': 'ANY', 'uid': '611800569950521616'}, 'data': [{'instType': 'SWAP', 'instId': 'BTC-USDT-SWAP', 'tgtCcy': '', 'ccy': '', 'ordId': '1993784914940116992', 'clOrdId': '', 'algoClOrdId': '', 'algoId': '', 'tag': '', 'px': '80000', 'sz': '0.1', 'notionalUsd': '80.0128', 'ordType': 'limit', 'side': 'buy', 'posSide': 'long', 'tdMode': 'cross', 'accFillSz': '0', 'fillNotionalUsd': '', 'avgPx': '0', 'state': 'canceled', 'lever': '3', 'pnl': '0', 'feeCcy': 'USDT', 'fee': '0', 'rebateCcy': 'USDT', 'rebate': '0', 'category': 'normal', 'uTime': '1731921825881', 'cTime': '1731921820806', 'source': '', 'reduceOnly': 'false', 'cancelSource': '1', 'quickMgnType': '', 'stpId': '', 'stpMode': 'cancel_maker', 'attachAlgoClOrdId': '', 'lastPx': '91880', 'isTpLimit': 'false', 'slTriggerPx': '', 'slTriggerPxType': '', 'tpOrdPx': '', 'tpTriggerPx': '', 'tpTriggerPxType': '', 'slOrdPx': '', 'fillPx': '', 'tradeId': '', 'fillSz': '0', 'fillTime': '', 'fillPnl': '0', 'fillFee': '0', 'fillFeeCcy': '', 'execType': '', 'fillPxVol': '', 'fillPxUsd': '', 'fillMarkVol': '', 'fillFwdPx': '', 'fillMarkPx': '', 'amendSource': '', 'reqId': '', 'amendResult': '', 'code': '0', 'msg': '', 'pxType': '', 'pxUsd': '', 'pxVol': '', 'linkedAlgoOrd': {'algoId': ''}, 'attachAlgoOrds': []}]}
202
+ """
203
+ endpoint = "/api/v5/trade/order"
204
+ payload = {
205
+ "instId": inst_id,
206
+ "tdMode": td_mode,
207
+ "side": side,
208
+ "ordType": ord_type,
209
+ "sz": sz,
210
+ **kwargs,
211
+ }
212
+ cost = self._get_rate_limit_cost(1)
213
+ await self._limiter.order_limit(endpoint, cost=cost)
214
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
215
+ return self._place_order_decoder.decode(raw)
216
+
217
+ async def post_api_v5_trade_cancel_order(
218
+ self, instId: str, clOrdId: str
219
+ ) -> OkxCancelOrderResponse:
220
+ """
221
+ Cancel an existing order
222
+ https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-order
223
+ """
224
+ endpoint = "/api/v5/trade/cancel-order"
225
+ payload = {"instId": instId, "clOrdId": clOrdId}
226
+
227
+ # cost = self._get_rate_limit_cost(1)
228
+ # await self._limiter.order_limit(endpoint, cost=cost)
229
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
230
+ return self._cancel_order_decoder.decode(raw)
231
+
232
+ async def post_api_v5_trade_cancel_batch_order(
233
+ self, payload: list[dict]
234
+ ) -> OkxCancelBatchOrderResponse:
235
+ """
236
+ Cancel multiple orders in batch
237
+ https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-cancel-batch-orders
238
+
239
+ Args:
240
+ payload: List of dictionaries, each containing:
241
+ - instId (str): Instrument ID, e.g. BTC-USDT
242
+ - ordId (str, optional): Order ID
243
+ - clOrdId (str, optional): Client Order ID
244
+ Note: Either ordId or clOrdId is required for each order
245
+
246
+ Returns:
247
+ OkxCancelBatchOrderResponse: Response containing cancellation results
248
+ """
249
+ endpoint = "/api/v5/trade/cancel-batch-orders"
250
+ # cost = len(payload)
251
+ # await self._limiter.order_limit(endpoint, cost=cost)
252
+ raw = await self._fetch(
253
+ "POST",
254
+ endpoint,
255
+ payload=payload,
256
+ signed=True,
257
+ handled_okx_codes=("0", "1", "2"),
258
+ )
259
+ return self._cancel_batch_order_response_decoder.decode(raw)
260
+
261
+ async def get_api_v5_market_history_index_candles(
262
+ self,
263
+ instId: str,
264
+ bar: str | None = None,
265
+ after: str | None = None,
266
+ before: str | None = None,
267
+ limit: int | None = None,
268
+ ) -> OkxIndexCandlesticksResponse:
269
+ """
270
+ GET /api/v5/market/history-index-candles
271
+ """
272
+ endpoint = "/api/v5/market/history-index-candles"
273
+ payload = {
274
+ "instId": instId,
275
+ "bar": bar.replace("candle", "") if bar else None,
276
+ "after": after,
277
+ "before": before,
278
+ "limit": str(limit) if limit is not None else None,
279
+ }
280
+ payload = {k: v for k, v in payload.items() if v is not None}
281
+ cost = self._get_rate_limit_cost(1)
282
+ await self._limiter.query_limit(endpoint, cost=cost)
283
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=False)
284
+ return self._index_candles_response_decoder.decode(raw)
285
+
286
+ async def get_api_v5_market_candles(
287
+ self,
288
+ instId: str,
289
+ bar: str | None = None,
290
+ after: str | None = None,
291
+ before: str | None = None,
292
+ limit: int | None = None,
293
+ ) -> OkxCandlesticksResponse:
294
+ # the default bar is 1m
295
+ endpoint = "/api/v5/market/candles"
296
+ payload = {
297
+ k: v
298
+ for k, v in {
299
+ "instId": instId,
300
+ "bar": bar.replace("candle", "") if bar else None,
301
+ "after": after,
302
+ "before": before,
303
+ "limit": str(limit),
304
+ }.items()
305
+ if v is not None
306
+ }
307
+ cost = self._get_rate_limit_cost(1)
308
+ await self._limiter.query_limit(endpoint, cost=cost)
309
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=False)
310
+ return self._candles_response_decoder.decode(raw)
311
+
312
+ async def get_api_v5_market_history_candles(
313
+ self,
314
+ instId: str,
315
+ bar: str | None = None,
316
+ after: str | None = None,
317
+ before: str | None = None,
318
+ limit: str | None = None,
319
+ ) -> OkxCandlesticksResponse:
320
+ # the default bar is 1m
321
+ endpoint = "/api/v5/market/history-candles"
322
+ payload = {
323
+ k: v
324
+ for k, v in {
325
+ "instId": instId,
326
+ "bar": bar.replace("candle", "") if bar else None,
327
+ "after": after,
328
+ "before": before,
329
+ "limit": str(limit),
330
+ }.items()
331
+ if v is not None
332
+ }
333
+ cost = self._get_rate_limit_cost(1)
334
+ await self._limiter.query_limit(endpoint, cost=cost)
335
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=False)
336
+ return self._candles_response_decoder.decode(raw)
337
+
338
+ async def get_api_v5_finance_savings_balance(
339
+ self,
340
+ ccy: str | None = None,
341
+ ) -> OkxSavingsBalanceResponse:
342
+ """
343
+ GET /api/v5/finance/savings/balance
344
+ """
345
+ endpoint = "/api/v5/finance/savings/balance"
346
+ payload = {"ccy": ccy} if ccy else None
347
+ raw = await self._fetch("GET", endpoint, payload=payload, signed=True)
348
+ return self._savings_balance_response_decoder.decode(raw)
349
+
350
+ async def post_api_v5_finance_savings_purchase_redempt(
351
+ self,
352
+ ccy: str,
353
+ amt: str,
354
+ side: str,
355
+ rate: str | None = None,
356
+ ) -> OkxSavingsPurchaseRedemptResponse:
357
+ """
358
+ POST /api/v5/finance/savings/purchase-redempt
359
+ """
360
+ endpoint = "/api/v5/finance/savings/purchase-redempt"
361
+ payload = {
362
+ "ccy": ccy,
363
+ "amt": amt,
364
+ "side": side,
365
+ }
366
+ if rate:
367
+ payload["rate"] = rate
368
+
369
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
370
+ return self._savings_purchase_redempt_response_decoder.decode(raw)
371
+
372
+ async def get_api_v5_finance_savings_lending_rate_summary(
373
+ self,
374
+ ccy: str | None = None,
375
+ ) -> OkxSavingsLendingRateSummaryResponse:
376
+ """
377
+ /api/v5/finance/savings/lending-rate-summary
378
+ """
379
+ endpoint = "/api/v5/finance/savings/lending-rate-summary"
380
+ payload = {"ccy": ccy} if ccy else None
381
+ raw = await self._fetch("GET", endpoint, payload=payload, signed=False)
382
+ return self._savings_lending_rate_summary_response_decoder.decode(raw)
383
+
384
+ async def get_api_v5_finance_savings_lending_rate_history(
385
+ self,
386
+ ccy: str | None = None,
387
+ after: str | None = None,
388
+ before: str | None = None,
389
+ limit: str | None = None,
390
+ ) -> OkxSavingsLendingRateHistoryResponse:
391
+ """
392
+ GET /api/v5/finance/savings/lending-rate-history
393
+ """
394
+ endpoint = "/api/v5/finance/savings/lending-rate-history"
395
+ payload = {
396
+ "ccy": ccy,
397
+ "after": after,
398
+ "before": before,
399
+ "limit": limit,
400
+ }
401
+ payload = {k: v for k, v in payload.items() if v is not None}
402
+ raw = await self._fetch("GET", endpoint, payload=payload, signed=False)
403
+ return self._savings_lending_rate_history_response_decoder.decode(raw)
404
+
405
+ async def post_api_v5_asset_transfer(
406
+ self,
407
+ ccy: str,
408
+ amt: str,
409
+ from_acct: str, # from
410
+ to_acct: str, # to
411
+ type: str = "0",
412
+ subAcct: Optional[str] = None,
413
+ loanTrans: bool = False,
414
+ omitPosRisk: bool = False,
415
+ clientId: Optional[str] = None,
416
+ ) -> OkxAssetTransferResponse:
417
+ """
418
+ POST /api/v5/asset/transfer
419
+ """
420
+ endpoint = "/api/v5/asset/transfer"
421
+ payload = {
422
+ "ccy": ccy,
423
+ "amt": amt,
424
+ "from": from_acct,
425
+ "to": to_acct,
426
+ "type": type,
427
+ "subAcct": subAcct,
428
+ "loanTrans": loanTrans,
429
+ "omitPosRisk": omitPosRisk,
430
+ "clientId": clientId,
431
+ }
432
+ payload = {k: v for k, v in payload.items() if v is not None}
433
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
434
+ return self._asset_transfer_response_decoder.decode(raw)
435
+
436
+ async def post_api_v5_trade_amend_order(
437
+ self,
438
+ instId: str,
439
+ cxlOnFail: bool = False,
440
+ ordId: Optional[str] = None,
441
+ clOrdId: Optional[str] = None,
442
+ reqId: Optional[str] = None,
443
+ newSz: Optional[str] = None,
444
+ newPx: Optional[str] = None,
445
+ newPxUsd: Optional[str] = None,
446
+ newPxVol: Optional[str] = None,
447
+ attachAlgoOrds: Optional[List[dict]] = None,
448
+ ):
449
+ """
450
+ https://www.okx.com/docs-v5/en/#order-book-trading-trade-post-amend-order
451
+ POST /api/v5/trade/amend-order
452
+ """
453
+ endpoint = "/api/v5/trade/amend-order"
454
+ payload = {
455
+ "instId": instId,
456
+ "cxlOnFail": cxlOnFail,
457
+ "ordId": ordId,
458
+ "clOrdId": clOrdId,
459
+ "reqId": reqId,
460
+ "newSz": newSz,
461
+ "newPx": newPx,
462
+ "newPxUsd": newPxUsd,
463
+ "newPxVol": newPxVol,
464
+ "attachAlgoOrds": attachAlgoOrds,
465
+ }
466
+ payload = {k: v for k, v in payload.items() if v is not None}
467
+ cost = self._get_rate_limit_cost(1)
468
+ await self._limiter.order_limit(endpoint, cost=cost)
469
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
470
+ return self._amend_order_response_decoder.decode(raw)
471
+
472
+ async def post_api_v5_finance_staking_defi_redeem(
473
+ self, ordId: str, protocolType: str, allowEarlyRedeem: bool = False
474
+ ):
475
+ """
476
+ POST /api/v5/finance/staking-defi/redeem
477
+ """
478
+ endpoint = "/api/v5/finance/staking-defi/redeem"
479
+ payload = {
480
+ "ordId": ordId,
481
+ "protocolType": protocolType,
482
+ "allowEarlyRedeem": allowEarlyRedeem,
483
+ }
484
+ payload = {k: v for k, v in payload.items() if v is not None}
485
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
486
+ return self._finance_staking_defi_redeem_response_decoder.decode(raw)
487
+
488
+ async def post_api_v5_trade_batch_orders(
489
+ self, payload: list[dict]
490
+ ) -> OkxBatchOrderResponse:
491
+ """
492
+ POST /api/v5/trade/batch-orders
493
+ """
494
+ endpoint = "/api/v5/trade/batch-orders"
495
+ cost = len(payload)
496
+ await self._limiter.order_limit(endpoint, cost=cost)
497
+ raw = await self._fetch(
498
+ "POST",
499
+ endpoint,
500
+ payload=payload,
501
+ signed=True,
502
+ handled_okx_codes=("0", "1", "2"),
503
+ )
504
+ return self._batch_order_response_decoder.decode(raw)
505
+
506
+ async def post_api_v5_finance_staking_defi_purchase(
507
+ self,
508
+ productId: str,
509
+ investData: list[dict],
510
+ term: Optional[str] = None,
511
+ tag: Optional[str] = None,
512
+ ):
513
+ """
514
+ productId String Yes Product ID
515
+ investData Array of objects Yes Investment data
516
+ > ccy String Yes Investment currency, e.g. BTC
517
+ > amt String Yes Investment amount
518
+ term String Conditional Investment term
519
+ Investment term must be specified for fixed-term product
520
+ tag String No Order tag
521
+ A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters.
522
+ """
523
+
524
+ endpoint = "/api/v5/finance/staking-defi/purchase"
525
+ payload = {
526
+ "productId": productId,
527
+ "investData": investData,
528
+ "term": term,
529
+ "tag": tag,
530
+ }
531
+ payload = {k: v for k, v in payload.items() if v is not None}
532
+ raw = await self._fetch("POST", endpoint, payload=payload, signed=True)
533
+ return self._finance_staking_defi_purchase_response_decoder.decode(raw)
534
+
535
+ async def get_api_v5_finance_staking_defi_offers(
536
+ self,
537
+ productId: Optional[str] = None,
538
+ protocolType: Optional[str] = None,
539
+ ccy: Optional[str] = None,
540
+ ):
541
+ endpoint = "/api/v5/finance/staking-defi/offers"
542
+ payload = {
543
+ "productId": productId,
544
+ "protocolType": protocolType,
545
+ "ccy": ccy,
546
+ }
547
+ payload = {k: v for k, v in payload.items() if v is not None}
548
+ raw = await self._fetch("GET", endpoint, payload=payload, signed=True)
549
+ return self._finance_staking_defi_offers_response_decoder.decode(raw)
550
+
551
+ async def get_api_v5_account_config(self):
552
+ """
553
+ GET /api/v5/account/config
554
+ """
555
+ endpoint = "/api/v5/account/config"
556
+ cost = self._get_rate_limit_cost(1)
557
+ await self._limiter.query_limit(endpoint, cost=cost)
558
+ raw = await self._fetch_async("GET", endpoint, signed=True)
559
+ return self._account_config_response_decoder.decode(raw)
560
+
561
+ def _generate_signature(self, message: str) -> str:
562
+ assert self._secret is not None
563
+ hex_digest = hmac_signature(self._secret, message)
564
+ digest = bytes.fromhex(hex_digest)
565
+ return base64.b64encode(digest).decode()
566
+
567
+ def _get_signature(
568
+ self, ts: str, method: str, request_path: str, payload: Optional[bytes]
569
+ ) -> str:
570
+ body = payload.decode() if payload else ""
571
+ sign_str = f"{ts}{method}{request_path}{body}"
572
+ signature = self._generate_signature(sign_str)
573
+ return signature
574
+
575
+ def _get_timestamp(self) -> str:
576
+ return (
577
+ self._clock.utc_now()
578
+ .isoformat(timespec="milliseconds")
579
+ .replace("+00:00", "Z")
580
+ )
581
+
582
+ def _get_headers(
583
+ self, ts: str, method: str, request_path: str, payload: Optional[bytes]
584
+ ) -> Dict[str, Any]:
585
+ assert self._api_key is not None and self._passphrase is not None
586
+ headers = self._headers
587
+ signature = self._get_signature(ts, method, request_path, payload)
588
+ headers.update(
589
+ {
590
+ "OK-ACCESS-KEY": self._api_key,
591
+ "OK-ACCESS-SIGN": signature,
592
+ "OK-ACCESS-TIMESTAMP": ts,
593
+ "OK-ACCESS-PASSPHRASE": self._passphrase,
594
+ }
595
+ )
596
+ return headers
597
+
598
+ async def _fetch(
599
+ self,
600
+ method: str,
601
+ endpoint: str,
602
+ payload: Union[Dict[str, Any], list, None] = None,
603
+ signed: bool = False,
604
+ handled_okx_codes: tuple[str, ...] = ("0",),
605
+ ) -> bytes:
606
+ """
607
+ Fetch data from the OKX API asynchronously.
608
+ """
609
+ assert self._retry_manager is not None
610
+ result = await self._retry_manager.run(
611
+ name=f"{method} {endpoint}",
612
+ func=self._fetch_async,
613
+ method=method,
614
+ endpoint=endpoint,
615
+ payload=payload,
616
+ signed=signed,
617
+ handled_okx_codes=handled_okx_codes,
618
+ )
619
+ assert result is not None
620
+ return result
621
+
622
+ async def _fetch_async(
623
+ self,
624
+ method: str,
625
+ endpoint: str,
626
+ payload: Union[Dict[str, Any], list, None] = None,
627
+ signed: bool = False,
628
+ handled_okx_codes: tuple[str, ...] = ("0",),
629
+ ) -> bytes:
630
+ await self._init_session(self._base_url)
631
+
632
+ request_path = endpoint
633
+ headers = self._headers
634
+ timestamp = self._get_timestamp()
635
+
636
+ payload = payload or {}
637
+
638
+ payload_json = (
639
+ urlencode(payload) if method == "GET" else msgspec.json.encode(payload)
640
+ )
641
+
642
+ if method == "GET":
643
+ if payload_json:
644
+ request_path += f"?{payload_json}"
645
+ payload_json = None
646
+
647
+ if signed and self._api_key:
648
+ headers = self._get_headers(
649
+ timestamp, method, request_path, cast(Optional[bytes], payload_json)
650
+ )
651
+
652
+ assert self._session is not None
653
+ try:
654
+ self._log.debug(
655
+ f"{method} {request_path} Headers: {headers} payload: {payload_json}"
656
+ )
657
+
658
+ async with self._session.request(
659
+ method=method,
660
+ url=request_path,
661
+ headers=headers,
662
+ data=payload_json,
663
+ ) as response:
664
+ raw = await response.read()
665
+
666
+ if response.status >= 400:
667
+ error_data = msgspec.json.decode(raw)
668
+ code = error_data.get("code", str(response.status))
669
+ msg = error_data.get("msg", "Unknown error")
670
+ raise OkxHttpError(
671
+ status_code=response.status,
672
+ message=f"code={code}, msg={msg}",
673
+ headers=response.headers,
674
+ )
675
+ okx_response = self._general_response_decoder.decode(raw)
676
+ if okx_response.code in handled_okx_codes:
677
+ return raw
678
+
679
+ okx_error_response = self._error_response_decoder.decode(raw)
680
+ for data in okx_error_response.data:
681
+ if data.sCode != "0":
682
+ raise OkxRequestError(
683
+ error_code=int(data.sCode),
684
+ status_code=response.status,
685
+ message=data.sMsg,
686
+ )
687
+ raise OkxRequestError(
688
+ error_code=int(okx_error_response.code),
689
+ status_code=response.status,
690
+ message=okx_error_response.msg,
691
+ )
692
+ except asyncio.TimeoutError as e:
693
+ self._log.error(f"Timeout {method} {request_path} {e}")
694
+ raise
695
+ except aiohttp.ClientError as e:
696
+ self._log.error(f"Request Error {method} {request_path} {e}")
697
+ raise
698
+ except Exception as e:
699
+ self._log.error(f"Error {method} {request_path} {e}")
700
+ raise
701
+
702
+ async def get_api_v5_market_tickers(
703
+ self,
704
+ inst_type: str,
705
+ uly: str | None = None,
706
+ inst_family: str | None = None,
707
+ ) -> OkxTickersResponse:
708
+ """
709
+ GET /api/v5/market/tickers
710
+
711
+ Retrieve the latest price snapshot, best bid/ask price, and trading volume
712
+ in the last 24 hours for multiple instruments.
713
+
714
+ Rate Limit: 20 requests per 2 seconds
715
+
716
+ Args:
717
+ inst_type: Instrument type (SPOT, SWAP, FUTURES, OPTION)
718
+ uly: Underlying (optional), e.g. BTC-USD. Applicable to FUTURES/SWAP/OPTION
719
+ inst_family: Instrument family (optional). Applicable to FUTURES/SWAP/OPTION
720
+
721
+ Returns:
722
+ OkxTickersResponse: Response containing ticker data for multiple instruments
723
+ """
724
+ endpoint = "/api/v5/market/tickers"
725
+ payload = {
726
+ "instType": inst_type,
727
+ "uly": uly,
728
+ "instFamily": inst_family,
729
+ }
730
+ payload = {k: v for k, v in payload.items() if v is not None}
731
+
732
+ cost = self._get_rate_limit_cost(1)
733
+ await self._limiter.query_limit(endpoint, cost=cost)
734
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=False)
735
+ return self._tickers_response_decoder.decode(raw)
736
+
737
+ async def get_api_v5_market_ticker(self, inst_id: str) -> OkxTickersResponse:
738
+ """
739
+ GET /api/v5/market/ticker
740
+
741
+ Retrieve the latest price snapshot, best bid/ask price, and trading volume
742
+ in the last 24 hours for a single instrument.
743
+
744
+ Rate Limit: 20 requests per 2 seconds
745
+
746
+ Args:
747
+ inst_id: Instrument ID (e.g., BTC-USD-SWAP)
748
+
749
+ Returns:
750
+ OkxTickersResponse: Response containing ticker data for the single instrument
751
+ """
752
+ endpoint = "/api/v5/market/ticker"
753
+ payload = {"instId": inst_id}
754
+
755
+ cost = self._get_rate_limit_cost(1)
756
+ await self._limiter.query_limit(endpoint, cost=cost)
757
+ raw = await self._fetch_async("GET", endpoint, payload=payload, signed=False)
758
+ return self._tickers_response_decoder.decode(raw)
759
+
760
+ async def get_api_v5_trade_order(
761
+ self,
762
+ inst_id: str,
763
+ ord_id: str | None = None,
764
+ cl_ord_id: str | None = None,
765
+ ) -> OkxOrderResponse:
766
+ """
767
+ GET /api/v5/trade/order
768
+
769
+ Retrieve order details. Either ordId or clOrdId is required.
770
+ If both are passed, ordId will be used.
771
+
772
+ Rate Limit: 60 requests per 2 seconds
773
+
774
+ Args:
775
+ inst_id: Instrument ID (e.g., BTC-USDT). Only applicable to live instruments
776
+ ord_id: Order ID. Either ordId or clOrdId is required
777
+ cl_ord_id: Client Order ID as assigned by the client. Either ordId or clOrdId is required
778
+
779
+ Returns:
780
+ OkxOrderResponse: Response containing order details
781
+
782
+ Raises:
783
+ ValueError: If neither ord_id nor cl_ord_id is provided
784
+ """
785
+ if not ord_id and not cl_ord_id:
786
+ raise ValueError("Either ord_id or cl_ord_id must be provided")
787
+
788
+ endpoint = "/api/v5/trade/order"
789
+ payload = {"instId": inst_id}
790
+
791
+ if ord_id:
792
+ payload["ordId"] = ord_id
793
+ if cl_ord_id:
794
+ payload["clOrdId"] = cl_ord_id
795
+
796
+ cost = self._get_rate_limit_cost(1)
797
+ await self._limiter.order_limit(endpoint, cost=cost)
798
+ raw = await self._fetch("GET", endpoint, payload=payload, signed=True)
799
+ return self._order_response_decoder.decode(raw)