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,518 @@
1
+ from enum import Enum, unique
2
+ from throttled.asyncio import Throttled, rate_limiter, RateLimiterType
3
+ from walrasquant.constants import (
4
+ AccountType,
5
+ OrderStatus,
6
+ PositionSide,
7
+ OrderSide,
8
+ TimeInForce,
9
+ OrderType,
10
+ KlineInterval,
11
+ RateLimiter,
12
+ TriggerType,
13
+ )
14
+ from walrasquant.error import KlineSupportedError
15
+ from walrasquant.exchange.okx.error import OkxRateLimitError
16
+
17
+
18
+ class OkxWsApiOp(Enum):
19
+ PLACE_ORDER = "order"
20
+ BATCH_ORDERS = "batch-orders"
21
+ CANCEL_ORDER = "cancel-order"
22
+
23
+ @property
24
+ def is_place_order(self):
25
+ return self == self.PLACE_ORDER
26
+
27
+ @property
28
+ def is_batch_orders(self):
29
+ return self == self.BATCH_ORDERS
30
+
31
+ @property
32
+ def is_cancel_order(self):
33
+ return self == self.CANCEL_ORDER
34
+
35
+
36
+ class OkxTriggerType(Enum):
37
+ NONE = ""
38
+ LAST_PRICE = "last"
39
+ INDEX_PRICE = "index"
40
+ MARK_PRICE = "mark"
41
+
42
+
43
+ class OkxAcctLv(Enum):
44
+ SPOT = "1"
45
+ FUTURES = "2"
46
+ MULTI_CURRENCY_MARGIN = "3"
47
+ PORTFOLIO_MARGIN = "4"
48
+
49
+ @property
50
+ def is_spot(self):
51
+ return self == self.SPOT
52
+
53
+ @property
54
+ def is_futures(self):
55
+ return self == self.FUTURES
56
+
57
+ @property
58
+ def is_multi_currency_margin(self):
59
+ return self == self.MULTI_CURRENCY_MARGIN
60
+
61
+ @property
62
+ def is_portfolio_margin(self):
63
+ return self == self.PORTFOLIO_MARGIN
64
+
65
+
66
+ class OkxPositionMode(Enum):
67
+ ONE_WAY_MODE = "net_mode"
68
+ LONG_SHORT_MODE = "long_short_mode"
69
+
70
+ @property
71
+ def is_one_way_mode(self):
72
+ return self == self.ONE_WAY_MODE
73
+
74
+ @property
75
+ def is_long_short_mode(self):
76
+ return self == self.LONG_SHORT_MODE
77
+
78
+
79
+ class OkxSavingsPurchaseRedemptSide(Enum):
80
+ PURCHASE = "purchase"
81
+ REDEMPT = "redempt"
82
+
83
+
84
+ class OkxKlineInterval(Enum):
85
+ SECOND_1 = "candle1s"
86
+ MINUTE_1 = "candle1m"
87
+ MINUTE_3 = "candle3m"
88
+ MINUTE_5 = "candle5m"
89
+ MINUTE_15 = "candle15m"
90
+ MINUTE_30 = "candle30m"
91
+ HOUR_1 = "candle1H"
92
+ HOUR_4 = "candle4H"
93
+ HOUR_6 = "candle6Hutc"
94
+ HOUR_12 = "candle12Hutc"
95
+ DAY_1 = "candle1Dutc"
96
+ DAY_3 = "candle3Dutc"
97
+ WEEK_1 = "candle1Wutc"
98
+ MONTH_1 = "candle1Mutc"
99
+
100
+
101
+ class OkxInstrumentType(Enum):
102
+ SPOT = "SPOT"
103
+ MARGIN = "MARGIN"
104
+ SWAP = "SWAP"
105
+ FUTURES = "FUTURES"
106
+ OPTION = "OPTION"
107
+ ANY = "ANY"
108
+
109
+
110
+ class OkxInstrumentFamily(Enum):
111
+ FUTURES = "FUTURES"
112
+ SWAP = "SWAP"
113
+ OPTION = "OPTION"
114
+
115
+
116
+ class OkxAccountType(AccountType):
117
+ LIVE = "live"
118
+ # AWS = "aws" # deprecated
119
+ DEMO = "demo"
120
+
121
+ @property
122
+ def exchange_id(self):
123
+ return "okx"
124
+
125
+ @property
126
+ def is_testnet(self):
127
+ return self == OkxAccountType.DEMO
128
+
129
+ @property
130
+ def stream_url(self):
131
+ return STREAM_URLS[self]
132
+
133
+
134
+ STREAM_URLS = {
135
+ OkxAccountType.LIVE: "wss://ws.okx.com:8443/ws",
136
+ OkxAccountType.DEMO: "wss://wspap.okx.com:8443/ws",
137
+ }
138
+
139
+ REST_URLS = {
140
+ OkxAccountType.LIVE: "https://www.okx.com",
141
+ OkxAccountType.DEMO: "https://www.okx.com",
142
+ }
143
+
144
+
145
+ @unique
146
+ class OkxTdMode(Enum):
147
+ CASH = "cash" # 现货
148
+ CROSS = "cross" # 全仓
149
+ ISOLATED = "isolated" # 逐仓
150
+ SPOT_ISOLATED = "spot_isolated" # 现货逐仓
151
+
152
+ @property
153
+ def is_cash(self):
154
+ return self == self.CASH
155
+
156
+ @property
157
+ def is_cross(self):
158
+ return self == self.CROSS
159
+
160
+ @property
161
+ def is_isolated(self):
162
+ return self == self.ISOLATED
163
+
164
+ @property
165
+ def is_spot_isolated(self):
166
+ return self == self.SPOT_ISOLATED
167
+
168
+
169
+ @unique
170
+ class OkxPositionSide(Enum):
171
+ LONG = "long"
172
+ SHORT = "short"
173
+ NET = "net"
174
+ NONE = ""
175
+
176
+ def parse_to_position_side(self) -> PositionSide:
177
+ if self == self.NET:
178
+ return PositionSide.FLAT
179
+ elif self == self.LONG:
180
+ return PositionSide.LONG
181
+ elif self == self.SHORT:
182
+ return PositionSide.SHORT
183
+ raise RuntimeError(f"Invalid position side: {self}")
184
+
185
+
186
+ @unique
187
+ class OkxOrderSide(Enum):
188
+ BUY = "buy"
189
+ SELL = "sell"
190
+
191
+ @property
192
+ def is_buy(self):
193
+ return self == self.BUY
194
+
195
+ @property
196
+ def is_sell(self):
197
+ return self == self.SELL
198
+
199
+
200
+ class OkxTimeInForce(Enum):
201
+ IOC = "ioc"
202
+ GTC = "gtc"
203
+ FOK = "fok"
204
+
205
+
206
+ @unique
207
+ class OkxOrderType(Enum):
208
+ MARKET = "market"
209
+ LIMIT = "limit"
210
+ POST_ONLY = "post_only" # limit only, requires "px" to be provided
211
+ FOK = "fok" # market order if "px" is not provided, otherwise limit order
212
+ IOC = "ioc" # market order if "px" is not provided, otherwise limit order
213
+ OPTIMAL_LIMIT_IOC = (
214
+ "optimal_limit_ioc" # Market order with immediate-or-cancel order
215
+ )
216
+ MMP = "mmp" # Market Maker Protection (only applicable to Option in Portfolio Margin mode)
217
+ MMP_AND_POST_ONLY = "mmp_and_post_only" # Market Maker Protection and Post-only order(only applicable to Option in Portfolio Margin mode)
218
+
219
+
220
+ @unique
221
+ class OkxOrderStatus(Enum): # "state"
222
+ CANCELED = "canceled"
223
+ LIVE = "live"
224
+ PARTIALLY_FILLED = "partially_filled"
225
+ FILLED = "filled"
226
+ MMP_CANCELED = "mmp_canceled"
227
+
228
+
229
+ class OkxEnumParser:
230
+ _okx_trigger_type_map = {
231
+ OkxTriggerType.LAST_PRICE: TriggerType.LAST_PRICE,
232
+ OkxTriggerType.INDEX_PRICE: TriggerType.INDEX_PRICE,
233
+ OkxTriggerType.MARK_PRICE: TriggerType.MARK_PRICE,
234
+ }
235
+
236
+ _okx_kline_interval_map = {
237
+ OkxKlineInterval.SECOND_1: KlineInterval.SECOND_1,
238
+ OkxKlineInterval.MINUTE_1: KlineInterval.MINUTE_1,
239
+ OkxKlineInterval.MINUTE_3: KlineInterval.MINUTE_3,
240
+ OkxKlineInterval.MINUTE_5: KlineInterval.MINUTE_5,
241
+ OkxKlineInterval.MINUTE_15: KlineInterval.MINUTE_15,
242
+ OkxKlineInterval.MINUTE_30: KlineInterval.MINUTE_30,
243
+ OkxKlineInterval.HOUR_1: KlineInterval.HOUR_1,
244
+ OkxKlineInterval.HOUR_4: KlineInterval.HOUR_4,
245
+ OkxKlineInterval.HOUR_6: KlineInterval.HOUR_6,
246
+ OkxKlineInterval.HOUR_12: KlineInterval.HOUR_12,
247
+ OkxKlineInterval.DAY_1: KlineInterval.DAY_1,
248
+ OkxKlineInterval.DAY_3: KlineInterval.DAY_3,
249
+ OkxKlineInterval.WEEK_1: KlineInterval.WEEK_1,
250
+ OkxKlineInterval.MONTH_1: KlineInterval.MONTH_1,
251
+ }
252
+
253
+ _okx_order_status_map = {
254
+ OkxOrderStatus.LIVE: OrderStatus.ACCEPTED,
255
+ OkxOrderStatus.PARTIALLY_FILLED: OrderStatus.PARTIALLY_FILLED,
256
+ OkxOrderStatus.FILLED: OrderStatus.FILLED,
257
+ OkxOrderStatus.CANCELED: OrderStatus.CANCELED,
258
+ OkxOrderStatus.MMP_CANCELED: OrderStatus.CANCELED,
259
+ }
260
+
261
+ _okx_position_side_map = {
262
+ OkxPositionSide.NET: PositionSide.FLAT,
263
+ OkxPositionSide.LONG: PositionSide.LONG,
264
+ OkxPositionSide.SHORT: PositionSide.SHORT,
265
+ OkxPositionSide.NONE: None,
266
+ }
267
+
268
+ _okx_order_side_map = {
269
+ OkxOrderSide.BUY: OrderSide.BUY,
270
+ OkxOrderSide.SELL: OrderSide.SELL,
271
+ }
272
+
273
+ # Add reverse mapping dictionaries
274
+ _order_status_to_okx_map = {v: k for k, v in _okx_order_status_map.items()}
275
+ _position_side_to_okx_map = {
276
+ PositionSide.FLAT: OkxPositionSide.NET,
277
+ PositionSide.LONG: OkxPositionSide.LONG,
278
+ PositionSide.SHORT: OkxPositionSide.SHORT,
279
+ }
280
+ _order_side_to_okx_map = {v: k for k, v in _okx_order_side_map.items()}
281
+
282
+ _kline_interval_to_okx_map = {v: k for k, v in _okx_kline_interval_map.items()}
283
+ _trigger_type_to_okx_map = {v: k for k, v in _okx_trigger_type_map.items()}
284
+
285
+ @classmethod
286
+ def parse_trigger_type(cls, trigger_type: OkxTriggerType) -> TriggerType:
287
+ return cls._okx_trigger_type_map[trigger_type]
288
+
289
+ @classmethod
290
+ def parse_kline_interval(cls, interval: OkxKlineInterval) -> KlineInterval:
291
+ return cls._okx_kline_interval_map[interval]
292
+
293
+ # Add reverse parsing methods
294
+ @classmethod
295
+ def parse_order_status(cls, status: OkxOrderStatus) -> OrderStatus:
296
+ return cls._okx_order_status_map[status]
297
+
298
+ @classmethod
299
+ def parse_position_side(cls, side: OkxPositionSide) -> PositionSide:
300
+ return cls._okx_position_side_map[side] or PositionSide.FLAT
301
+
302
+ @classmethod
303
+ def parse_order_side(cls, side: OkxOrderSide) -> OrderSide:
304
+ return cls._okx_order_side_map[side]
305
+
306
+ @classmethod
307
+ def parse_order_type(cls, ordType: OkxOrderType) -> OrderType:
308
+ # TODO add parameters in future to enable parsing of all other nautilus OrderType's
309
+ match ordType:
310
+ case OkxOrderType.MARKET:
311
+ return OrderType.MARKET
312
+ case OkxOrderType.LIMIT:
313
+ return OrderType.LIMIT
314
+ case OkxOrderType.IOC:
315
+ return OrderType.LIMIT
316
+ case OkxOrderType.FOK:
317
+ return OrderType.LIMIT
318
+ case OkxOrderType.POST_ONLY:
319
+ return OrderType.POST_ONLY
320
+ case _:
321
+ raise NotImplementedError(
322
+ f"Cannot parse OrderType from OKX order type {ordType}"
323
+ )
324
+
325
+ @classmethod
326
+ def parse_time_in_force(cls, ordType: OkxOrderType) -> TimeInForce:
327
+ match ordType:
328
+ case OkxOrderType.MARKET:
329
+ return TimeInForce.GTC
330
+ case OkxOrderType.LIMIT:
331
+ return TimeInForce.GTC
332
+ case OkxOrderType.POST_ONLY:
333
+ return TimeInForce.GTC
334
+ case OkxOrderType.FOK:
335
+ return TimeInForce.FOK
336
+ case OkxOrderType.IOC:
337
+ return TimeInForce.IOC
338
+ case _:
339
+ raise NotImplementedError(
340
+ f"Cannot parse TimeInForce from OKX order type {ordType}"
341
+ )
342
+
343
+ @classmethod
344
+ def to_okx_order_status(cls, status: OrderStatus) -> OkxOrderStatus:
345
+ return cls._order_status_to_okx_map[status]
346
+
347
+ @classmethod
348
+ def to_okx_position_side(cls, side: PositionSide) -> OkxPositionSide:
349
+ return cls._position_side_to_okx_map[side]
350
+
351
+ @classmethod
352
+ def to_okx_order_side(cls, side: OrderSide) -> OkxOrderSide:
353
+ return cls._order_side_to_okx_map[side]
354
+
355
+ @classmethod
356
+ def to_okx_trigger_type(cls, trigger_type: TriggerType) -> OkxTriggerType:
357
+ return cls._trigger_type_to_okx_map[trigger_type]
358
+
359
+ @classmethod
360
+ def to_okx_order_type(
361
+ cls, order_type: OrderType, time_in_force: TimeInForce
362
+ ) -> OkxOrderType:
363
+ if order_type == OrderType.MARKET:
364
+ return OkxOrderType.MARKET
365
+ elif order_type == OrderType.POST_ONLY:
366
+ return OkxOrderType.POST_ONLY
367
+
368
+ match time_in_force:
369
+ case TimeInForce.GTC:
370
+ return OkxOrderType.LIMIT # OKX limit orders are GTC by default
371
+ case TimeInForce.FOK:
372
+ return OkxOrderType.FOK
373
+ case TimeInForce.IOC:
374
+ return OkxOrderType.IOC
375
+ case _:
376
+ raise RuntimeError(
377
+ f"Could not determine OKX order type from order_type {order_type} and time_in_force {time_in_force}, valid OKX order types are: {list(OkxOrderType)}",
378
+ )
379
+
380
+ @classmethod
381
+ def to_okx_kline_interval(cls, interval: KlineInterval) -> OkxKlineInterval:
382
+ if interval not in cls._kline_interval_to_okx_map:
383
+ raise KlineSupportedError(
384
+ f"Kline interval {interval} is not supported by OKX"
385
+ )
386
+ return cls._kline_interval_to_okx_map[interval]
387
+
388
+
389
+ class OkxRateLimiter(RateLimiter):
390
+ def __init__(self, enable_rate_limit: bool = True):
391
+ self._enabled = enable_rate_limit
392
+ self._throttled: dict[str, Throttled] = {
393
+ "/api/v5/trade/order": Throttled(
394
+ quota=rate_limiter.per_sec(30),
395
+ timeout=-1,
396
+ using=RateLimiterType.GCRA.value,
397
+ ),
398
+ "/api/v5/trade/cancel-order": Throttled(
399
+ quota=rate_limiter.per_sec(30),
400
+ timeout=-1,
401
+ using=RateLimiterType.GCRA.value,
402
+ ),
403
+ "/api/v5/trade/amend-order": Throttled(
404
+ quota=rate_limiter.per_sec(30),
405
+ timeout=-1,
406
+ using=RateLimiterType.GCRA.value,
407
+ ),
408
+ "/api/v5/trade/batch-orders": Throttled(
409
+ quota=rate_limiter.per_sec(150),
410
+ timeout=-1,
411
+ using=RateLimiterType.GCRA.value,
412
+ ),
413
+ "/api/v5/trade/cancel-batch-orders": Throttled(
414
+ quota=rate_limiter.per_sec(150),
415
+ timeout=-1,
416
+ using=RateLimiterType.GCRA.value,
417
+ ),
418
+ "/ws/order": Throttled(
419
+ quota=rate_limiter.per_sec(30),
420
+ timeout=-1,
421
+ using=RateLimiterType.GCRA.value,
422
+ ),
423
+ "/ws/cancel": Throttled(
424
+ quota=rate_limiter.per_sec(30),
425
+ timeout=-1,
426
+ using=RateLimiterType.GCRA.value,
427
+ ),
428
+ "/api/v5/account/balance": Throttled(
429
+ quota=rate_limiter.per_sec(5, burst=1),
430
+ timeout=-1,
431
+ using=RateLimiterType.GCRA.value,
432
+ ),
433
+ "/api/v5/account/positions": Throttled(
434
+ quota=rate_limiter.per_sec(5, burst=1),
435
+ timeout=-1,
436
+ using=RateLimiterType.GCRA.value,
437
+ ),
438
+ "/api/v5/market/candles": Throttled(
439
+ quota=rate_limiter.per_sec(20, burst=1),
440
+ timeout=-1,
441
+ using=RateLimiterType.GCRA.value,
442
+ ),
443
+ "/api/v5/market/history-candles": Throttled(
444
+ quota=rate_limiter.per_sec(10, burst=1),
445
+ timeout=-1,
446
+ using=RateLimiterType.GCRA.value,
447
+ ),
448
+ "/api/v5/account/config": Throttled(
449
+ quota=rate_limiter.per_sec(2, burst=1),
450
+ timeout=-1,
451
+ using=RateLimiterType.GCRA.value,
452
+ ),
453
+ "/api/v5/market/history-index-candles": Throttled(
454
+ quota=rate_limiter.per_sec(4, burst=1),
455
+ timeout=-1,
456
+ using=RateLimiterType.GCRA.value,
457
+ ),
458
+ "/api/v5/market/tickers": Throttled(
459
+ quota=rate_limiter.per_sec(10, burst=1),
460
+ timeout=-1,
461
+ using=RateLimiterType.GCRA.value,
462
+ ),
463
+ "/api/v5/market/ticker": Throttled(
464
+ quota=rate_limiter.per_sec(10, burst=1),
465
+ timeout=-1,
466
+ using=RateLimiterType.GCRA.value,
467
+ ),
468
+ }
469
+
470
+ @staticmethod
471
+ def _raise_if_limited(
472
+ result, message: str, scope: str, endpoint: str | None = None
473
+ ):
474
+ if result.limited:
475
+ raise OkxRateLimitError(
476
+ message,
477
+ retry_after=result.state.retry_after,
478
+ scope=scope,
479
+ endpoint=endpoint,
480
+ )
481
+
482
+ async def order_limit(self, endpoint: str, cost: int = 1):
483
+ if not self._enabled:
484
+ return
485
+ result = await self._throttled[endpoint].limit(
486
+ key=endpoint, cost=cost, timeout=-1
487
+ )
488
+ self._raise_if_limited(
489
+ result,
490
+ f"OKX order rate limit exceeded: {endpoint}",
491
+ scope="uid",
492
+ endpoint=endpoint,
493
+ )
494
+
495
+ async def query_limit(self, endpoint: str, cost: int = 1):
496
+ if not self._enabled:
497
+ return
498
+ result = await self._throttled[endpoint].limit(
499
+ key=endpoint, cost=cost, timeout=1
500
+ )
501
+ self._raise_if_limited(
502
+ result,
503
+ f"OKX query rate limit exceeded: {endpoint}",
504
+ scope="uid",
505
+ endpoint=endpoint,
506
+ )
507
+
508
+
509
+ def strip_uuid_hyphens(uuid_str: str) -> str:
510
+ """Remove hyphens from UUID string for OKX API compatibility."""
511
+ return uuid_str.replace("-", "")
512
+
513
+
514
+ def restore_uuid_hyphens(uuid_str: str) -> str:
515
+ """Restore hyphens to UUID string from OKX API response."""
516
+ if len(uuid_str) != 32:
517
+ return uuid_str # Return as-is if not a valid stripped UUID
518
+ return f"{uuid_str[:8]}-{uuid_str[8:12]}-{uuid_str[12:16]}-{uuid_str[16:20]}-{uuid_str[20:]}"
@@ -0,0 +1,144 @@
1
+ from decimal import Decimal
2
+ from typing import Dict, Literal, cast
3
+ from walrasquant.constants import AccountType
4
+ from walrasquant.schema import InstrumentId, BaseMarket
5
+ from walrasquant.core.cache import AsyncCache
6
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
7
+ from walrasquant.core.entity import TaskManager
8
+ from walrasquant.core.registry import OrderRegistry
9
+ from walrasquant.exchange.okx import OkxAccountType
10
+ from walrasquant.exchange.okx.schema import OkxMarket
11
+ from walrasquant.base import ExecutionManagementSystem
12
+ from walrasquant.base.ems import PriorityOrderQueue
13
+
14
+
15
+ class OkxExecutionManagementSystem(ExecutionManagementSystem):
16
+ _market: Dict[str, OkxMarket]
17
+
18
+ OKX_ACCOUNT_TYPE_PRIORITY = [
19
+ OkxAccountType.DEMO,
20
+ # OkxAccountType.AWS,
21
+ OkxAccountType.LIVE,
22
+ ]
23
+
24
+ def __init__(
25
+ self,
26
+ market: Dict[str, OkxMarket],
27
+ cache: AsyncCache,
28
+ msgbus: MessageBus,
29
+ clock: LiveClock,
30
+ task_manager: TaskManager,
31
+ registry: OrderRegistry,
32
+ queue_maxsize: int = 100_000,
33
+ ):
34
+ super().__init__(
35
+ market=market,
36
+ cache=cache,
37
+ msgbus=msgbus,
38
+ clock=clock,
39
+ task_manager=task_manager,
40
+ registry=registry,
41
+ queue_maxsize=queue_maxsize,
42
+ )
43
+ self._okx_account_type: OkxAccountType | None = None
44
+
45
+ def _build_order_submit_queues(self):
46
+ for account_type in self._private_connectors.keys():
47
+ if isinstance(account_type, OkxAccountType):
48
+ self._order_submit_queues[account_type] = PriorityOrderQueue(
49
+ maxsize=self._queue_maxsize
50
+ )
51
+ break
52
+
53
+ def _set_account_type(self):
54
+ account_types = self._private_connectors.keys()
55
+ for account_type in self.OKX_ACCOUNT_TYPE_PRIORITY:
56
+ if account_type in account_types:
57
+ self._okx_account_type = account_type
58
+ break
59
+
60
+ def _instrument_id_to_account_type(
61
+ self, instrument_id: InstrumentId
62
+ ) -> AccountType:
63
+ if self._okx_account_type is None:
64
+ raise ValueError("No OKX account type configured")
65
+ return self._okx_account_type
66
+
67
+ def _get_min_order_amount(
68
+ self, symbol: str, market: BaseMarket, px: float
69
+ ) -> Decimal:
70
+ okx_market = cast(OkxMarket, market)
71
+ amount_limits = okx_market.limits.amount
72
+ min_order_amount = amount_limits.min if amount_limits is not None else 0.0
73
+ min_order_amount = min_order_amount or 0.0
74
+ min_order_amount = super()._amount_to_precision(
75
+ symbol, min_order_amount, mode="ceil"
76
+ )
77
+
78
+ if not okx_market.spot:
79
+ # for linear and inverse, the min order amount is contract size and ctVal is base amount per contract
80
+ min_order_amount *= Decimal(okx_market.info.ctVal or "1")
81
+
82
+ return min_order_amount
83
+
84
+ # override the base method
85
+ def _amount_to_precision(
86
+ self,
87
+ symbol: str,
88
+ amount: float,
89
+ mode: Literal["round"] | Literal["ceil"] | Literal["floor"] = "round",
90
+ ) -> Decimal:
91
+ market = self._market[symbol]
92
+ ctVal = Decimal("1")
93
+ if not market.spot:
94
+ ctVal = Decimal(market.info.ctVal or "1")
95
+ adjusted_amount = float(Decimal(str(amount)) / ctVal)
96
+ return super()._amount_to_precision(symbol, adjusted_amount, mode) * ctVal
97
+ return super()._amount_to_precision(symbol, amount, mode)
98
+
99
+ def _get_max_order_amount(
100
+ self, symbol: str, market: BaseMarket, is_market: bool, px: float
101
+ ) -> Decimal:
102
+ # maxLmtSz String The maximum order quantity of a single limit order. # If it is a derivatives contract, the value is the number of contracts. # If it is SPOT/MARGIN, the value is the quantity in base currency.
103
+ # maxMktSz String The maximum order quantity of a single market order. # If it is a derivatives contract, the value is the number of contracts. # If it is SPOT/MARGIN, the value is the quantity in USDT.
104
+ # maxLmtAmt String Max USD amount for a single limit order
105
+ # maxMktAmt String Max USD amount for a single market order # Only applicable to SPOT/MARGIN
106
+ okx_market = cast(OkxMarket, market)
107
+ ctVal = Decimal(okx_market.info.ctVal or "1")
108
+ if is_market:
109
+ maxMktSz = okx_market.info.maxMktSz or "Infinity"
110
+ maxMktAmt = okx_market.info.maxMktAmt or "Infinity"
111
+
112
+ if not okx_market.spot:
113
+ return Decimal(maxMktSz) * ctVal
114
+ else:
115
+ _MinMktSzAmt = min(float(maxMktSz), float(maxMktAmt))
116
+ return self._amount_to_precision(
117
+ symbol, _MinMktSzAmt / px, mode="floor"
118
+ )
119
+ else:
120
+ maxLmtSz = (
121
+ okx_market.info.maxLmtSz or "Infinity"
122
+ ) # SPOT/MARGIN: quantity in base currency, DERIVATIVE: number of contracts
123
+ maxLmtAmt = okx_market.info.maxLmtAmt
124
+
125
+ if not okx_market.spot:
126
+ _maxLmtSz = Decimal(maxLmtSz) * ctVal
127
+ _maxLmtAmt = (
128
+ self._amount_to_precision(
129
+ symbol, float(maxLmtAmt) / px, mode="floor"
130
+ )
131
+ if maxLmtAmt
132
+ else Decimal("Infinity")
133
+ )
134
+ return min(_maxLmtSz, _maxLmtAmt)
135
+ else:
136
+ _maxLmtSz = Decimal(maxLmtSz)
137
+ _maxLmtAmt = (
138
+ self._amount_to_precision(
139
+ symbol, float(maxLmtAmt) / px, mode="floor"
140
+ )
141
+ if maxLmtAmt
142
+ else Decimal("Infinity")
143
+ )
144
+ return min(_maxLmtSz, _maxLmtAmt)