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,479 @@
1
+ from datetime import timedelta
2
+ from throttled.asyncio import Throttled, rate_limiter, RateLimiterType
3
+ from throttled import Throttled as ThrottledSync
4
+
5
+ from walrasquant.constants import (
6
+ AccountType,
7
+ OrderStatus,
8
+ PositionSide,
9
+ OrderSide,
10
+ TimeInForce,
11
+ OrderType,
12
+ KlineInterval,
13
+ RateLimiter,
14
+ TriggerType,
15
+ )
16
+ from enum import Enum
17
+ from walrasquant.error import KlineSupportedError
18
+ from walrasquant.exchange.bybit.error import BybitRateLimitError
19
+
20
+
21
+ class BybitOpType(Enum):
22
+ AUTH = "auth"
23
+ PING = "ping"
24
+ PONG = "pong"
25
+ ORDER_CREATE = "order.create"
26
+ ORDER_AMEND = "order.amend"
27
+ ORDER_CANCEL = "order.cancel"
28
+ ORDER_CREATE_BATCH = "order.create-batch"
29
+ ORDER_AMEND_BATCH = "order.amend-batch"
30
+ ORDER_CANCEL_BATCH = "order.cancel-batch"
31
+
32
+
33
+ class BybitKlineInterval(Enum):
34
+ MINUTE_1 = "1"
35
+ MINUTE_3 = "3"
36
+ MINUTE_5 = "5"
37
+ MINUTE_15 = "15"
38
+ MINUTE_30 = "30"
39
+ HOUR_1 = "60"
40
+ HOUR_2 = "120"
41
+ HOUR_4 = "240"
42
+ HOUR_6 = "360"
43
+ HOUR_12 = "720"
44
+ DAY_1 = "D"
45
+ WEEK_1 = "W"
46
+ MONTH_1 = "M"
47
+
48
+
49
+ class BybitAccountType(AccountType):
50
+ SPOT = "SPOT"
51
+ LINEAR = "LINEAR"
52
+ INVERSE = "INVERSE"
53
+ OPTION = "OPTION"
54
+ SPOT_TESTNET = "SPOT_TESTNET"
55
+ LINEAR_TESTNET = "LINEAR_TESTNET"
56
+ INVERSE_TESTNET = "INVERSE_TESTNET"
57
+ OPTION_TESTNET = "OPTION_TESTNET"
58
+ UNIFIED = "UNIFIED"
59
+ UNIFIED_TESTNET = "UNIFIED_TESTNET"
60
+
61
+ @property
62
+ def exchange_id(self):
63
+ return "bybit"
64
+
65
+ @property
66
+ def is_testnet(self):
67
+ return self in {
68
+ self.SPOT_TESTNET,
69
+ self.LINEAR_TESTNET,
70
+ self.INVERSE_TESTNET,
71
+ self.OPTION_TESTNET,
72
+ self.UNIFIED_TESTNET,
73
+ }
74
+
75
+ @property
76
+ def ws_public_url(self):
77
+ return WS_PUBLIC_URL[self]
78
+
79
+ @property
80
+ def ws_private_url(self):
81
+ if self.is_testnet:
82
+ return "wss://stream-testnet.bybit.com/v5/private"
83
+ return "wss://stream.bybit.com/v5/private"
84
+
85
+ @property
86
+ def ws_api_url(self):
87
+ if self.is_testnet:
88
+ return "wss://stream-testnet.bybit.com/v5/trade"
89
+ return "wss://stream.bybit.com/v5/trade"
90
+
91
+ @property
92
+ def is_spot(self):
93
+ return self in {self.SPOT, self.SPOT_TESTNET}
94
+
95
+ @property
96
+ def is_linear(self):
97
+ return self in {self.LINEAR, self.LINEAR_TESTNET}
98
+
99
+ @property
100
+ def is_inverse(self):
101
+ return self in {self.INVERSE, self.INVERSE_TESTNET}
102
+
103
+
104
+ WS_PUBLIC_URL = {
105
+ BybitAccountType.SPOT: "wss://stream.bybit.com/v5/public/spot",
106
+ BybitAccountType.LINEAR: "wss://stream.bybit.com/v5/public/linear",
107
+ BybitAccountType.INVERSE: "wss://stream.bybit.com/v5/public/inverse",
108
+ BybitAccountType.OPTION: "wss://stream.bybit.com/v5/public/option",
109
+ BybitAccountType.SPOT_TESTNET: "wss://stream-testnet.bybit.com/v5/public/spot",
110
+ BybitAccountType.LINEAR_TESTNET: "wss://stream-testnet.bybit.com/v5/public/linear",
111
+ BybitAccountType.INVERSE_TESTNET: "wss://stream-testnet.bybit.com/v5/public/inverse",
112
+ BybitAccountType.OPTION_TESTNET: "wss://stream-testnet.bybit.com/v5/public/option",
113
+ }
114
+
115
+
116
+ class BybitBaseUrl(Enum):
117
+ MAINNET_1 = "MAINNET_1"
118
+ MAINNET_2 = "MAINNET_2"
119
+ TESTNET = "TESTNET"
120
+ NETHERLAND = "NETHERLAND"
121
+ HONGKONG = "HONGKONG"
122
+ TURKEY = "TURKEY"
123
+ HAZAKHSTAN = "HAZAKHSTAN"
124
+
125
+ @property
126
+ def base_url(self):
127
+ return REST_API_URL[self]
128
+
129
+
130
+ REST_API_URL = {
131
+ BybitBaseUrl.MAINNET_1: "https://api.bybit.com",
132
+ BybitBaseUrl.MAINNET_2: "https://api.bytick.com",
133
+ BybitBaseUrl.TESTNET: "https://api-testnet.bybit.com",
134
+ BybitBaseUrl.NETHERLAND: "https://api.bybit.nl",
135
+ BybitBaseUrl.HONGKONG: "https://api.byhkbit.com",
136
+ BybitBaseUrl.TURKEY: "https://api.bybit-tr.com",
137
+ BybitBaseUrl.HAZAKHSTAN: "https://api.bybit.kz",
138
+ }
139
+
140
+
141
+ class BybitOrderSide(Enum):
142
+ BUY = "Buy"
143
+ SELL = "Sell"
144
+
145
+
146
+ class BybitOrderStatus(Enum):
147
+ NEW = "New"
148
+ REJECTED = "Rejected"
149
+ PARTIALLY_FILLED = "PartiallyFilled"
150
+ PARTIALLY_FILLED_CANCELED = "PartiallyFilledCanceled"
151
+ FILLED = "Filled"
152
+ CANCELED = "Cancelled"
153
+ UNTRIGGERED = "Untriggered"
154
+ TRIGGERED = "Triggered"
155
+ DEACTIVATED = "Deactivated"
156
+
157
+
158
+ class BybitTimeInForce(Enum):
159
+ GTC = "GTC"
160
+ IOC = "IOC"
161
+ FOK = "FOK"
162
+ POST_ONLY = "PostOnly"
163
+
164
+
165
+ class BybitPositionIdx(Enum):
166
+ FLAT = 0
167
+ LONG = 1
168
+ SHORT = 2
169
+
170
+ def is_one_way_mode(self):
171
+ return self == self.FLAT
172
+
173
+
174
+ class BybitPositionSide(Enum):
175
+ FLAT = ""
176
+ BUY = "Buy"
177
+ SELL = "Sell"
178
+
179
+ def parse_to_position_side(self) -> PositionSide:
180
+ if self == self.FLAT:
181
+ return PositionSide.FLAT
182
+ elif self == self.BUY:
183
+ return PositionSide.LONG
184
+ elif self == self.SELL:
185
+ return PositionSide.SHORT
186
+ raise RuntimeError(f"Invalid position side: {self}")
187
+
188
+
189
+ class BybitOrderType(Enum):
190
+ MARKET = "Market"
191
+ LIMIT = "Limit"
192
+ UNKNOWN = "Unknown"
193
+
194
+
195
+ class BybitProductType(Enum):
196
+ SPOT = "spot"
197
+ LINEAR = "linear"
198
+ INVERSE = "inverse"
199
+ OPTION = "option"
200
+
201
+ @property
202
+ def is_spot(self):
203
+ return self == self.SPOT
204
+
205
+ @property
206
+ def is_linear(self):
207
+ return self == self.LINEAR
208
+
209
+ @property
210
+ def is_inverse(self):
211
+ return self == self.INVERSE
212
+
213
+ @property
214
+ def is_option(self):
215
+ return self == self.OPTION
216
+
217
+
218
+ class BybitTriggerType(Enum):
219
+ NONE = "" # Default
220
+ LAST_PRICE = "LastPrice"
221
+ INDEX_PRICE = "IndexPrice"
222
+ MARK_PRICE = "MarkPrice"
223
+
224
+
225
+ class BybitTriggerDirection(Enum):
226
+ NONE = 0
227
+ RISES_TO = 1 # Triggered when market price rises to triggerPrice
228
+ FALLS_TO = 2
229
+
230
+
231
+ class BybitStopOrderType(Enum):
232
+ NONE = "" # Default
233
+ UNKNOWN = "UNKNOWN" # Classic account value
234
+ TAKE_PROFIT = "TakeProfit"
235
+ STOP_LOSS = "StopLoss"
236
+ TRAILING_STOP = "TrailingStop"
237
+ STOP = "Stop"
238
+ PARTIAL_TAKE_PROFIT = "PartialTakeProfit"
239
+ PARTIAL_STOP_LOSS = "PartialStopLoss"
240
+ TPSL_ORDER = "tpslOrder"
241
+ OCO_ORDER = "OcoOrder" # Spot only
242
+ MM_RATE_CLOSE = "MmRateClose"
243
+ BIDIRECTIONAL_TPSL_ORDER = "BidirectionalTpslOrder"
244
+
245
+
246
+ class BybitEnumParser:
247
+ _bybit_kline_interval_map = {
248
+ BybitKlineInterval.MINUTE_1: KlineInterval.MINUTE_1,
249
+ BybitKlineInterval.MINUTE_3: KlineInterval.MINUTE_3,
250
+ BybitKlineInterval.MINUTE_5: KlineInterval.MINUTE_5,
251
+ BybitKlineInterval.MINUTE_15: KlineInterval.MINUTE_15,
252
+ BybitKlineInterval.MINUTE_30: KlineInterval.MINUTE_30,
253
+ BybitKlineInterval.HOUR_1: KlineInterval.HOUR_1,
254
+ BybitKlineInterval.HOUR_2: KlineInterval.HOUR_2,
255
+ BybitKlineInterval.HOUR_4: KlineInterval.HOUR_4,
256
+ BybitKlineInterval.HOUR_6: KlineInterval.HOUR_6,
257
+ BybitKlineInterval.HOUR_12: KlineInterval.HOUR_12,
258
+ BybitKlineInterval.DAY_1: KlineInterval.DAY_1,
259
+ BybitKlineInterval.WEEK_1: KlineInterval.WEEK_1,
260
+ BybitKlineInterval.MONTH_1: KlineInterval.MONTH_1,
261
+ }
262
+
263
+ _bybit_order_status_map = {
264
+ BybitOrderStatus.NEW: OrderStatus.ACCEPTED,
265
+ BybitOrderStatus.PARTIALLY_FILLED: OrderStatus.PARTIALLY_FILLED,
266
+ BybitOrderStatus.PARTIALLY_FILLED_CANCELED: OrderStatus.CANCELED,
267
+ BybitOrderStatus.FILLED: OrderStatus.FILLED,
268
+ BybitOrderStatus.CANCELED: OrderStatus.CANCELED,
269
+ BybitOrderStatus.TRIGGERED: OrderStatus.ACCEPTED,
270
+ BybitOrderStatus.UNTRIGGERED: OrderStatus.PENDING,
271
+ BybitOrderStatus.DEACTIVATED: OrderStatus.EXPIRED,
272
+ BybitOrderStatus.REJECTED: OrderStatus.FAILED,
273
+ }
274
+
275
+ _bybit_position_side_map = {
276
+ BybitPositionIdx.FLAT: PositionSide.FLAT,
277
+ BybitPositionIdx.LONG: PositionSide.LONG,
278
+ BybitPositionIdx.SHORT: PositionSide.SHORT,
279
+ }
280
+
281
+ _bybit_order_side_map = {
282
+ BybitOrderSide.BUY: OrderSide.BUY,
283
+ BybitOrderSide.SELL: OrderSide.SELL,
284
+ }
285
+
286
+ _bybit_order_time_in_force_map = {
287
+ BybitTimeInForce.IOC: TimeInForce.IOC,
288
+ BybitTimeInForce.GTC: TimeInForce.GTC,
289
+ BybitTimeInForce.FOK: TimeInForce.FOK,
290
+ BybitTimeInForce.POST_ONLY: TimeInForce.GTC,
291
+ }
292
+
293
+ _bybit_order_type_map = {
294
+ BybitOrderType.MARKET: OrderType.MARKET,
295
+ BybitOrderType.LIMIT: OrderType.LIMIT,
296
+ }
297
+
298
+ _bybit_trigger_type_map = {
299
+ BybitTriggerType.LAST_PRICE: TriggerType.LAST_PRICE,
300
+ BybitTriggerType.INDEX_PRICE: TriggerType.INDEX_PRICE,
301
+ BybitTriggerType.MARK_PRICE: TriggerType.MARK_PRICE,
302
+ }
303
+
304
+ # Add reverse mapping dictionaries
305
+ _order_status_to_bybit_map = {v: k for k, v in _bybit_order_status_map.items()}
306
+ # Ensure CANCELED maps to the right status
307
+ _order_status_to_bybit_map[OrderStatus.CANCELED] = BybitOrderStatus.CANCELED
308
+ _position_side_to_bybit_map = {v: k for k, v in _bybit_position_side_map.items()}
309
+ _order_side_to_bybit_map = {v: k for k, v in _bybit_order_side_map.items()}
310
+ _time_in_force_to_bybit_map = {
311
+ v: k for k, v in _bybit_order_time_in_force_map.items()
312
+ }
313
+ _time_in_force_to_bybit_map[TimeInForce.GTC] = BybitTimeInForce.GTC
314
+ _order_type_to_bybit_map = {
315
+ v: k for k, v in _bybit_order_type_map.items() if v is not None
316
+ }
317
+ _kline_interval_to_bybit_map = {
318
+ v: k for k, v in _bybit_kline_interval_map.items() if v is not None
319
+ }
320
+ _trigger_type_to_bybit_map = {
321
+ v: k for k, v in _bybit_trigger_type_map.items() if v is not None
322
+ }
323
+
324
+ @classmethod
325
+ def parse_trigger_type(cls, trigger_type: BybitTriggerType) -> TriggerType:
326
+ return cls._bybit_trigger_type_map[trigger_type]
327
+
328
+ @classmethod
329
+ def parse_kline_interval(cls, interval: BybitKlineInterval) -> KlineInterval:
330
+ return cls._bybit_kline_interval_map[interval]
331
+
332
+ # Add reverse parsing methods
333
+ @classmethod
334
+ def parse_order_status(cls, status: BybitOrderStatus) -> OrderStatus:
335
+ return cls._bybit_order_status_map[status]
336
+
337
+ @classmethod
338
+ def parse_position_side(cls, side: BybitPositionIdx) -> PositionSide:
339
+ return cls._bybit_position_side_map[side]
340
+
341
+ @classmethod
342
+ def parse_order_side(cls, side: BybitOrderSide) -> OrderSide:
343
+ return cls._bybit_order_side_map[side]
344
+
345
+ @classmethod
346
+ def parse_time_in_force(cls, tif: BybitTimeInForce) -> TimeInForce:
347
+ return cls._bybit_order_time_in_force_map[tif]
348
+
349
+ @classmethod
350
+ def parse_order_type(
351
+ cls, order_type: BybitOrderType, time_in_force: TimeInForce | None = None
352
+ ) -> OrderType:
353
+ if time_in_force == BybitTimeInForce.POST_ONLY:
354
+ return OrderType.POST_ONLY
355
+ return cls._bybit_order_type_map[order_type]
356
+
357
+ @classmethod
358
+ def to_bybit_order_status(cls, status: OrderStatus) -> BybitOrderStatus:
359
+ return cls._order_status_to_bybit_map[status]
360
+
361
+ @classmethod
362
+ def to_bybit_position_side(cls, side: PositionSide) -> BybitPositionIdx:
363
+ return cls._position_side_to_bybit_map[side]
364
+
365
+ @classmethod
366
+ def to_bybit_order_side(cls, side: OrderSide) -> BybitOrderSide:
367
+ return cls._order_side_to_bybit_map[side]
368
+
369
+ @classmethod
370
+ def to_bybit_time_in_force(cls, tif: TimeInForce) -> BybitTimeInForce:
371
+ return cls._time_in_force_to_bybit_map[tif]
372
+
373
+ @classmethod
374
+ def to_bybit_order_type(cls, order_type: OrderType) -> BybitOrderType:
375
+ return cls._order_type_to_bybit_map[order_type]
376
+
377
+ @classmethod
378
+ def to_bybit_kline_interval(cls, interval: KlineInterval) -> BybitKlineInterval:
379
+ if interval not in cls._kline_interval_to_bybit_map:
380
+ raise KlineSupportedError(
381
+ f"Kline interval {interval} is not supported by Bybit"
382
+ )
383
+ return cls._kline_interval_to_bybit_map[interval]
384
+
385
+ @classmethod
386
+ def to_bybit_trigger_type(cls, trigger_type: TriggerType) -> BybitTriggerType:
387
+ return cls._trigger_type_to_bybit_map[trigger_type]
388
+
389
+
390
+ class BybitRateLimiter(RateLimiter):
391
+ def __init__(self, enable_rate_limit: bool = True):
392
+ self._enabled = enable_rate_limit
393
+ self._global_ip = Throttled(
394
+ quota=rate_limiter.per_duration(timedelta(seconds=5), limit=600),
395
+ timeout=-1,
396
+ using=RateLimiterType.GCRA.value,
397
+ )
398
+ self._spot_order = Throttled(
399
+ quota=rate_limiter.per_sec(20),
400
+ timeout=-1,
401
+ using=RateLimiterType.GCRA.value,
402
+ )
403
+ self._futures_order = Throttled(
404
+ quota=rate_limiter.per_sec(10),
405
+ timeout=-1,
406
+ using=RateLimiterType.GCRA.value,
407
+ )
408
+ self._query = Throttled(
409
+ quota=rate_limiter.per_sec(50),
410
+ timeout=-1,
411
+ using=RateLimiterType.GCRA.value,
412
+ )
413
+ self._public = Throttled(
414
+ quota=rate_limiter.per_duration(timedelta(seconds=5), limit=600),
415
+ timeout=-1,
416
+ using=RateLimiterType.GCRA.value,
417
+ )
418
+
419
+ @staticmethod
420
+ def _raise_if_limited(
421
+ result, message: str, scope: str, endpoint: str | None = None
422
+ ):
423
+ if result.limited:
424
+ raise BybitRateLimitError(
425
+ message,
426
+ retry_after=result.state.retry_after,
427
+ scope=scope,
428
+ endpoint=endpoint,
429
+ )
430
+
431
+ async def ip_limit(self):
432
+ if not self._enabled:
433
+ return
434
+ result = await self._global_ip.limit(key="ip", cost=1, timeout=5)
435
+ self._raise_if_limited(result, "Bybit IP rate limit exceeded", scope="ip")
436
+
437
+ async def query_limit(self, endpoint: str, cost: int = 1):
438
+ if not self._enabled:
439
+ return
440
+ result = await self._query.limit(key=endpoint, cost=cost, timeout=1)
441
+ self._raise_if_limited(
442
+ result,
443
+ f"Bybit query rate limit exceeded: {endpoint}",
444
+ scope="uid",
445
+ endpoint=endpoint,
446
+ )
447
+
448
+ async def public_limit(self, endpoint: str, cost: int = 1):
449
+ if not self._enabled:
450
+ return
451
+ result = await self._public.limit(key=endpoint, cost=cost, timeout=5)
452
+ self._raise_if_limited(
453
+ result,
454
+ f"Bybit public rate limit exceeded: {endpoint}",
455
+ scope="ip",
456
+ endpoint=endpoint,
457
+ )
458
+
459
+ async def order_limit(self, category: str, endpoint: str, cost: int = 1):
460
+ if not self._enabled:
461
+ return
462
+ if category == "spot":
463
+ result = await self._spot_order.limit(key=endpoint, cost=cost, timeout=-1)
464
+ self._raise_if_limited(
465
+ result,
466
+ f"Bybit spot order rate limit exceeded: {endpoint}",
467
+ scope="uid",
468
+ endpoint=endpoint,
469
+ )
470
+ else:
471
+ result = await self._futures_order.limit(
472
+ key=endpoint, cost=cost, timeout=-1
473
+ )
474
+ self._raise_if_limited(
475
+ result,
476
+ f"Bybit futures order rate limit exceeded: {endpoint}",
477
+ scope="uid",
478
+ endpoint=endpoint,
479
+ )
@@ -0,0 +1,93 @@
1
+ from typing import Dict, cast
2
+ from decimal import Decimal
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.bybit import BybitAccountType
10
+ from walrasquant.exchange.bybit.schema import BybitMarket
11
+ from walrasquant.base import ExecutionManagementSystem
12
+ from walrasquant.base.ems import PriorityOrderQueue
13
+
14
+
15
+ class BybitExecutionManagementSystem(ExecutionManagementSystem):
16
+ _market: Dict[str, BybitMarket]
17
+
18
+ def __init__(
19
+ self,
20
+ market: Dict[str, BybitMarket],
21
+ cache: AsyncCache,
22
+ msgbus: MessageBus,
23
+ clock: LiveClock,
24
+ task_manager: TaskManager,
25
+ registry: OrderRegistry,
26
+ queue_maxsize: int = 100_000,
27
+ ):
28
+ super().__init__(
29
+ market=market,
30
+ cache=cache,
31
+ msgbus=msgbus,
32
+ clock=clock,
33
+ task_manager=task_manager,
34
+ registry=registry,
35
+ queue_maxsize=queue_maxsize,
36
+ )
37
+ self._bybit_account_type: BybitAccountType | None = None
38
+
39
+ def _build_order_submit_queues(self):
40
+ # if not self._private_connectors:
41
+ # raise RuntimeError(
42
+ # "No private connectors found for building order submit queues."
43
+ # )
44
+ for account_type in self._private_connectors.keys():
45
+ if isinstance(account_type, BybitAccountType):
46
+ self._order_submit_queues[account_type] = PriorityOrderQueue(
47
+ maxsize=self._queue_maxsize
48
+ )
49
+
50
+ def _set_account_type(self):
51
+ # if not self._private_connectors:
52
+ # raise RuntimeError(
53
+ # "No private connectors found for setting account type."
54
+ # )
55
+ account_types = self._private_connectors.keys()
56
+ self._bybit_account_type = (
57
+ BybitAccountType.UNIFIED_TESTNET
58
+ if BybitAccountType.UNIFIED_TESTNET in account_types
59
+ else BybitAccountType.UNIFIED
60
+ )
61
+
62
+ def _instrument_id_to_account_type(
63
+ self, instrument_id: InstrumentId
64
+ ) -> AccountType:
65
+ if self._bybit_account_type is None:
66
+ raise ValueError("No Bybit account type configured")
67
+ return self._bybit_account_type
68
+
69
+ def _get_min_order_amount(
70
+ self, symbol: str, market: BaseMarket, px: float
71
+ ) -> Decimal:
72
+ bybit_market = cast(BybitMarket, market)
73
+ lot_size_filter = bybit_market.info.lotSizeFilter
74
+ if lot_size_filter is None:
75
+ raise ValueError(f"Missing lotSizeFilter for {symbol}")
76
+ min_order_qty = float(lot_size_filter.minOrderQty or "0")
77
+ min_order_amt = float(
78
+ lot_size_filter.minOrderAmt or lot_size_filter.minNotionalValue or "0"
79
+ )
80
+ min_order_amount = max(min_order_amt * 1.02 / px, min_order_qty)
81
+ min_order_amount = self._amount_to_precision(
82
+ symbol, min_order_amount, mode="ceil"
83
+ )
84
+ return min_order_amount
85
+
86
+ def _get_max_order_amount(
87
+ self, symbol: str, market: BaseMarket, is_market: bool, px: float
88
+ ) -> Decimal:
89
+ bybit_market = cast(BybitMarket, market)
90
+ if is_market:
91
+ return Decimal(bybit_market.info.lotSizeFilter.maxMktOrderQty or "Infinity")
92
+ else:
93
+ return Decimal(bybit_market.info.lotSizeFilter.maxOrderQty or "Infinity")
@@ -0,0 +1,36 @@
1
+ class BybitError(Exception):
2
+ """
3
+ Represents Bybit specific errors.
4
+ """
5
+
6
+ def __init__(
7
+ self,
8
+ code: int | None,
9
+ message: str | None,
10
+ ) -> None:
11
+ super().__init__(message)
12
+ self.code = code
13
+ self.message = message
14
+
15
+ def __repr__(self) -> str:
16
+ return f"{type(self).__name__}(code={self.code}, message='{self.message}')"
17
+
18
+ __str__ = __repr__
19
+
20
+
21
+ class BybitRateLimitError(Exception):
22
+ """
23
+ Raised when Bybit API rate limit is exceeded.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ message: str,
29
+ retry_after: float = 0.0,
30
+ scope: str | None = None,
31
+ endpoint: str | None = None,
32
+ ):
33
+ super().__init__(message)
34
+ self.retry_after = retry_after
35
+ self.scope = scope
36
+ self.endpoint = endpoint