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,392 @@
1
+ from enum import Enum
2
+ from walrasquant.constants import AccountType
3
+ from throttled.asyncio import Throttled, rate_limiter, RateLimiterType
4
+
5
+ from walrasquant.constants import (
6
+ OrderStatus,
7
+ PositionSide,
8
+ OrderSide,
9
+ TimeInForce,
10
+ OrderType,
11
+ KlineInterval,
12
+ RateLimiter,
13
+ )
14
+ from walrasquant.error import KlineSupportedError
15
+ from walrasquant.exchange.bitget.error import BitgetRateLimitError
16
+
17
+
18
+ class BitgetAccountType(AccountType):
19
+ UTA = "UTA"
20
+ SPOT = "SPOT"
21
+ FUTURE = "FUTURE"
22
+ UTA_DEMO = "UTA_DEMO"
23
+ SPOT_DEMO = "SPOT_DEMO"
24
+ FUTURE_DEMO = "FUTURE_DEMO"
25
+
26
+ @property
27
+ def exchange_id(self):
28
+ return "bitget"
29
+
30
+ @property
31
+ def is_testnet(self):
32
+ return self in (
33
+ self.UTA_DEMO,
34
+ self.SPOT_DEMO,
35
+ self.FUTURE_DEMO,
36
+ )
37
+
38
+ @property
39
+ def stream_url(self):
40
+ version = "v3" if self.is_uta else "v2"
41
+ if self.is_testnet:
42
+ return f"wss://wspap.bitget.com/{version}/ws"
43
+ else:
44
+ return f"wss://ws.bitget.com/{version}/ws"
45
+
46
+ @property
47
+ def is_spot(self):
48
+ return self in (self.SPOT, self.SPOT_DEMO)
49
+
50
+ @property
51
+ def is_future(self):
52
+ return self in (
53
+ self.FUTURE,
54
+ self.FUTURE_DEMO,
55
+ )
56
+
57
+ @property
58
+ def is_uta(self):
59
+ return self in (self.UTA, self.UTA_DEMO)
60
+
61
+
62
+ class BitgetInstType(Enum):
63
+ SPOT = "SPOT"
64
+ USDT_FUTURES = "USDT-FUTURES"
65
+ COIN_FUTURES = "COIN-FUTURES"
66
+ USDC_FUTURES = "USDC-FUTURES"
67
+ # SUSDT_FUTURES = "SUSD-FUTURES" # deprecated
68
+ # SUSDC_FUTURES = "SUSDC-FUTURES" # deprecated
69
+ # SCOIN_FUTURES = "SCOIN-FUTURES" # deprecated
70
+
71
+ @property
72
+ def is_spot(self) -> bool:
73
+ return self == BitgetInstType.SPOT
74
+
75
+ @property
76
+ def is_swap(self) -> bool:
77
+ return not self.is_spot
78
+
79
+ @property
80
+ def is_linear(self) -> bool:
81
+ return self in (BitgetInstType.USDT_FUTURES, BitgetInstType.USDC_FUTURES)
82
+
83
+ @property
84
+ def is_inverse(self) -> bool:
85
+ return self == BitgetInstType.COIN_FUTURES
86
+
87
+ @property
88
+ def is_usdc_swap(self) -> bool:
89
+ return self == BitgetInstType.USDC_FUTURES
90
+
91
+ @property
92
+ def is_usdt_swap(self) -> bool:
93
+ return self == BitgetInstType.USDT_FUTURES
94
+
95
+
96
+ class BitgetUtaInstType(Enum):
97
+ SPOT = "spot"
98
+ USDT_FUTURES = "usdt-futures"
99
+ COIN_FUTURES = "coin-futures"
100
+ USDC_FUTURES = "usdc-futures"
101
+ UTA = "UTA"
102
+
103
+ @property
104
+ def is_spot(self) -> bool:
105
+ return self == BitgetInstType.SPOT
106
+
107
+ @property
108
+ def is_swap(self) -> bool:
109
+ return not self.is_spot
110
+
111
+ @property
112
+ def is_linear(self) -> bool:
113
+ return self in (BitgetInstType.USDT_FUTURES, BitgetInstType.USDC_FUTURES)
114
+
115
+ @property
116
+ def is_inverse(self) -> bool:
117
+ return self == BitgetInstType.COIN_FUTURES
118
+
119
+ @property
120
+ def is_usdc_swap(self) -> bool:
121
+ return self == BitgetInstType.USDC_FUTURES
122
+
123
+ @property
124
+ def is_usdt_swap(self) -> bool:
125
+ return self == BitgetInstType.USDT_FUTURES
126
+
127
+
128
+ class BitgetKlineInterval(Enum):
129
+ MINUTE_1 = "1m"
130
+ MINUTE_5 = "5m"
131
+ MINUTE_15 = "15m"
132
+ MINUTE_30 = "30m"
133
+ HOUR_1 = "1H"
134
+ HOUR_4 = "4H"
135
+ HOUR_6 = "6H"
136
+ HOUR_12 = "12H"
137
+ DAY_1 = "1D"
138
+
139
+
140
+ class BitgetOrderSide(Enum):
141
+ BUY = "buy"
142
+ SELL = "sell"
143
+
144
+
145
+ class BitgetOrderStatus(Enum):
146
+ LIVE = "live"
147
+ NEW = "new"
148
+ FILLED = "filled"
149
+ CANCELED = "canceled"
150
+ CANCELLED = "cancelled" # Alias pointing to the same value
151
+ PARTIALLY_FILLED = "partially_filled"
152
+
153
+
154
+ class BitgetTimeInForce(Enum):
155
+ GTC = "gtc"
156
+ IOC = "ioc"
157
+ FOK = "fok"
158
+ POST_ONLY = "post_only"
159
+
160
+ @property
161
+ def is_gtc(self) -> bool:
162
+ return self == BitgetTimeInForce.GTC
163
+
164
+ @property
165
+ def is_ioc(self) -> bool:
166
+ return self == BitgetTimeInForce.IOC
167
+
168
+ @property
169
+ def is_fok(self) -> bool:
170
+ return self == BitgetTimeInForce.FOK
171
+
172
+ @property
173
+ def is_post_only(self) -> bool:
174
+ return self == BitgetTimeInForce.POST_ONLY
175
+
176
+
177
+ class BitgetOrderType(Enum):
178
+ LIMIT = "limit"
179
+ MARKET = "market"
180
+
181
+ @property
182
+ def is_limit(self) -> bool:
183
+ return self == BitgetOrderType.LIMIT
184
+
185
+ @property
186
+ def is_market(self) -> bool:
187
+ return self == BitgetOrderType.MARKET
188
+
189
+
190
+ class BitgetPositionSide(Enum):
191
+ LONG = "long"
192
+ SHORT = "short"
193
+ NET = "net" # one-way mode position
194
+
195
+ @property
196
+ def is_long(self) -> bool:
197
+ return self == BitgetPositionSide.LONG
198
+
199
+ @property
200
+ def is_short(self) -> bool:
201
+ return self == BitgetPositionSide.SHORT
202
+
203
+ @property
204
+ def is_net(self) -> bool:
205
+ return self == BitgetPositionSide.NET
206
+
207
+ def parse_to_position_side(self) -> PositionSide:
208
+ if self == self.LONG:
209
+ return PositionSide.LONG
210
+ elif self == self.SHORT:
211
+ return PositionSide.SHORT
212
+ else:
213
+ return PositionSide.FLAT
214
+
215
+
216
+ class BitgetEnumParser:
217
+ _kline_interval_map = {
218
+ BitgetKlineInterval.MINUTE_1: KlineInterval.MINUTE_1,
219
+ BitgetKlineInterval.MINUTE_5: KlineInterval.MINUTE_5,
220
+ BitgetKlineInterval.MINUTE_15: KlineInterval.MINUTE_15,
221
+ BitgetKlineInterval.MINUTE_30: KlineInterval.MINUTE_30,
222
+ BitgetKlineInterval.HOUR_1: KlineInterval.HOUR_1,
223
+ BitgetKlineInterval.HOUR_4: KlineInterval.HOUR_4,
224
+ BitgetKlineInterval.HOUR_6: KlineInterval.HOUR_6,
225
+ BitgetKlineInterval.HOUR_12: KlineInterval.HOUR_12,
226
+ BitgetKlineInterval.DAY_1: KlineInterval.DAY_1,
227
+ }
228
+
229
+ _order_status_map = {
230
+ BitgetOrderStatus.LIVE: OrderStatus.ACCEPTED,
231
+ BitgetOrderStatus.NEW: OrderStatus.ACCEPTED,
232
+ BitgetOrderStatus.FILLED: OrderStatus.FILLED,
233
+ BitgetOrderStatus.CANCELED: OrderStatus.CANCELED,
234
+ BitgetOrderStatus.CANCELLED: OrderStatus.CANCELED, # Alias pointing to the same value
235
+ BitgetOrderStatus.PARTIALLY_FILLED: OrderStatus.PARTIALLY_FILLED,
236
+ }
237
+
238
+ _order_side_map = {
239
+ BitgetOrderSide.BUY: OrderSide.BUY,
240
+ BitgetOrderSide.SELL: OrderSide.SELL,
241
+ }
242
+
243
+ _time_in_force_map = {
244
+ BitgetTimeInForce.GTC: TimeInForce.GTC,
245
+ BitgetTimeInForce.IOC: TimeInForce.IOC,
246
+ BitgetTimeInForce.FOK: TimeInForce.FOK,
247
+ }
248
+
249
+ _order_type_map = {
250
+ BitgetOrderType.LIMIT: OrderType.LIMIT,
251
+ BitgetOrderType.MARKET: OrderType.MARKET,
252
+ }
253
+
254
+ _kline_interval_to_bitget_map = {v: k for k, v in _kline_interval_map.items()}
255
+ _order_side_to_bitget_map = {v: k for k, v in _order_side_map.items()}
256
+ _time_in_force_to_bitget_map = {v: k for k, v in _time_in_force_map.items()}
257
+
258
+ @classmethod
259
+ def parse_kline_interval(cls, interval: BitgetKlineInterval) -> KlineInterval:
260
+ return cls._kline_interval_map[interval]
261
+
262
+ @classmethod
263
+ def to_bitget_kline_interval(cls, interval: KlineInterval) -> BitgetKlineInterval:
264
+ bitget_interval = cls._kline_interval_to_bitget_map.get(interval)
265
+ if bitget_interval is None:
266
+ raise KlineSupportedError(
267
+ f"Unsupported KlineInterval: {interval}. Supported intervals: {list(cls._kline_interval_to_bitget_map.keys())}"
268
+ )
269
+ return bitget_interval
270
+
271
+ @classmethod
272
+ def parse_order_status(cls, status: BitgetOrderStatus) -> OrderStatus:
273
+ # we do not care the UTA account NEW order status, so we ignore it
274
+ # ref: https://www.bitget.com/zh-CN/api-doc/uta/websocket/private/Order-Channel
275
+ order_status = cls._order_status_map.get(status)
276
+ if order_status is None:
277
+ raise ValueError(f"Unsupported Bitget order status: {status}")
278
+ return order_status
279
+
280
+ @classmethod
281
+ def parse_order_side(cls, side: BitgetOrderSide) -> OrderSide:
282
+ return cls._order_side_map[side]
283
+
284
+ @classmethod
285
+ def to_bitget_order_side(cls, side: OrderSide) -> BitgetOrderSide:
286
+ return cls._order_side_to_bitget_map[side]
287
+
288
+ @classmethod
289
+ def parse_time_in_force(cls, tif: BitgetTimeInForce) -> TimeInForce:
290
+ return cls._time_in_force_map[tif]
291
+
292
+ @classmethod
293
+ def to_bitget_time_in_force(cls, tif: TimeInForce) -> BitgetTimeInForce:
294
+ return cls._time_in_force_to_bitget_map[tif]
295
+
296
+ @classmethod
297
+ def parse_order_type(cls, order_type: BitgetOrderType) -> OrderType:
298
+ return cls._order_type_map[order_type]
299
+
300
+
301
+ class BitgetRateLimiter(RateLimiter):
302
+ def __init__(self, enable_rate_limit: bool = True):
303
+ self._enabled = enable_rate_limit
304
+ self._global_ip = Throttled(
305
+ quota=rate_limiter.per_min(6000),
306
+ timeout=-1,
307
+ using=RateLimiterType.FIXED_WINDOW.value,
308
+ )
309
+ self._throttled: dict[str, Throttled] = {
310
+ "/api/v2/mix/order/place-order": Throttled(
311
+ quota=rate_limiter.per_sec(10),
312
+ timeout=-1,
313
+ using=RateLimiterType.FIXED_WINDOW.value,
314
+ ),
315
+ "/api/v2/spot/trade/place-order": Throttled(
316
+ quota=rate_limiter.per_sec(10),
317
+ timeout=-1,
318
+ using=RateLimiterType.FIXED_WINDOW.value,
319
+ ),
320
+ "/api/v2/mix/order/cancel-order": Throttled(
321
+ quota=rate_limiter.per_sec(10),
322
+ timeout=-1,
323
+ using=RateLimiterType.FIXED_WINDOW.value,
324
+ ),
325
+ "/api/v2/spot/trade/cancel-order": Throttled(
326
+ quota=rate_limiter.per_sec(10),
327
+ timeout=-1,
328
+ using=RateLimiterType.FIXED_WINDOW.value,
329
+ ),
330
+ "/api/v3/trade/place-order": Throttled(
331
+ quota=rate_limiter.per_sec(10),
332
+ timeout=-1,
333
+ using=RateLimiterType.FIXED_WINDOW.value,
334
+ ),
335
+ "/api/v3/trade/cancel-order": Throttled(
336
+ quota=rate_limiter.per_sec(10),
337
+ timeout=-1,
338
+ using=RateLimiterType.FIXED_WINDOW.value,
339
+ ),
340
+ "/api/v3/trade/cancel-symbol-order": Throttled(
341
+ quota=rate_limiter.per_sec(5),
342
+ timeout=-1,
343
+ using=RateLimiterType.FIXED_WINDOW.value,
344
+ ),
345
+ }
346
+ self._query = Throttled(
347
+ quota=rate_limiter.per_sec(20),
348
+ timeout=-1,
349
+ using=RateLimiterType.FIXED_WINDOW.value,
350
+ )
351
+
352
+ @staticmethod
353
+ def _raise_if_limited(
354
+ result, message: str, scope: str, endpoint: str | None = None
355
+ ):
356
+ if result.limited:
357
+ raise BitgetRateLimitError(
358
+ message,
359
+ retry_after=result.state.retry_after,
360
+ scope=scope,
361
+ endpoint=endpoint,
362
+ )
363
+
364
+ async def ip_limit(self):
365
+ if not self._enabled:
366
+ return
367
+ result = await self._global_ip.limit(key="ip", cost=1, timeout=60)
368
+ self._raise_if_limited(result, "Bitget IP rate limit exceeded", scope="ip")
369
+
370
+ async def query_limit(self, endpoint: str, cost: int = 1):
371
+ if not self._enabled:
372
+ return
373
+ result = await self._query.limit(key=endpoint, cost=cost, timeout=1)
374
+ self._raise_if_limited(
375
+ result,
376
+ f"Bitget query rate limit exceeded: {endpoint}",
377
+ scope="uid",
378
+ endpoint=endpoint,
379
+ )
380
+
381
+ async def order_limit(self, endpoint: str, cost: int = 1):
382
+ if not self._enabled:
383
+ return
384
+ result = await self._throttled[endpoint].limit(
385
+ key=endpoint, cost=cost, timeout=-1
386
+ )
387
+ self._raise_if_limited(
388
+ result,
389
+ f"Bitget order rate limit exceeded: {endpoint}",
390
+ scope="uid",
391
+ endpoint=endpoint,
392
+ )
@@ -0,0 +1,202 @@
1
+ from typing import Dict, cast
2
+ from decimal import Decimal
3
+ from typing import Literal
4
+ from decimal import ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
5
+
6
+ from walrasquant.constants import AccountType
7
+ from walrasquant.schema import (
8
+ InstrumentId,
9
+ CancelAllOrderSubmit,
10
+ CancelOrderSubmit,
11
+ BaseMarket,
12
+ )
13
+ from walrasquant.core.cache import AsyncCache
14
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
15
+ from walrasquant.core.entity import TaskManager
16
+ from walrasquant.core.registry import OrderRegistry
17
+ from walrasquant.exchange.bitget import BitgetAccountType
18
+ from walrasquant.exchange.bitget.schema import BitgetMarket
19
+ from walrasquant.base import ExecutionManagementSystem
20
+ from walrasquant.base.ems import PriorityOrderQueue
21
+
22
+
23
+ class BitgetExecutionManagementSystem(ExecutionManagementSystem):
24
+ _market: Dict[str, BitgetMarket]
25
+
26
+ def __init__(
27
+ self,
28
+ market: Dict[str, BitgetMarket],
29
+ cache: AsyncCache,
30
+ msgbus: MessageBus,
31
+ clock: LiveClock,
32
+ task_manager: TaskManager,
33
+ registry: OrderRegistry,
34
+ queue_maxsize: int = 100_000,
35
+ ):
36
+ super().__init__(
37
+ market=market,
38
+ cache=cache,
39
+ msgbus=msgbus,
40
+ clock=clock,
41
+ task_manager=task_manager,
42
+ registry=registry,
43
+ queue_maxsize=queue_maxsize,
44
+ )
45
+
46
+ self._bitget_futures_account_type: BitgetAccountType | None = None
47
+ self._bitget_spot_account_type: BitgetAccountType | None = None
48
+ self._bitget_uta_account_type: BitgetAccountType | None = None
49
+
50
+ def _build_order_submit_queues(self):
51
+ for account_type in self._private_connectors.keys():
52
+ if isinstance(account_type, BitgetAccountType):
53
+ self._order_submit_queues[account_type] = PriorityOrderQueue(
54
+ maxsize=self._queue_maxsize
55
+ )
56
+
57
+ def _set_account_type(self):
58
+ account_types = self._private_connectors.keys()
59
+
60
+ if (
61
+ BitgetAccountType.UTA_DEMO in account_types
62
+ or BitgetAccountType.UTA in account_types
63
+ ):
64
+ self._bitget_uta_account_type = (
65
+ BitgetAccountType.UTA_DEMO
66
+ if BitgetAccountType.UTA_DEMO in account_types
67
+ else BitgetAccountType.UTA
68
+ )
69
+ return
70
+
71
+ self._bitget_futures_account_type = (
72
+ BitgetAccountType.FUTURE_DEMO
73
+ if BitgetAccountType.FUTURE_DEMO in account_types
74
+ else BitgetAccountType.FUTURE
75
+ )
76
+
77
+ self._bitget_spot_account_type = (
78
+ BitgetAccountType.SPOT_DEMO
79
+ if BitgetAccountType.SPOT_DEMO in account_types
80
+ else BitgetAccountType.SPOT
81
+ )
82
+
83
+ def _instrument_id_to_account_type(
84
+ self, instrument_id: InstrumentId
85
+ ) -> AccountType:
86
+ if self._bitget_uta_account_type:
87
+ return self._bitget_uta_account_type
88
+ if instrument_id.is_spot:
89
+ account_type = self._bitget_spot_account_type
90
+ elif instrument_id.is_linear or instrument_id.is_inverse:
91
+ account_type = self._bitget_futures_account_type
92
+ else:
93
+ raise ValueError(f"Unsupported instrument type: {instrument_id.type}")
94
+
95
+ if account_type is None:
96
+ raise ValueError(f"No Bitget account type configured for {instrument_id}")
97
+ return account_type
98
+
99
+ def _get_min_order_amount(
100
+ self, symbol: str, market: BaseMarket, px: float
101
+ ) -> Decimal:
102
+ bitget_market = cast(BitgetMarket, market)
103
+ if bitget_market.spot:
104
+ min_trade_usdt = bitget_market.info.minTradeUSDT or "0"
105
+ min_order_cost = float(min_trade_usdt)
106
+ min_order_amount = min_order_cost * 1.01 / px
107
+ else:
108
+ min_trade_num = bitget_market.info.minTradeNum or "0"
109
+ min_trade_usdt = bitget_market.info.minTradeUSDT or "0"
110
+ min_order_amt = float(min_trade_num)
111
+ min_order_cost = float(min_trade_usdt)
112
+ min_order_amount = max(min_order_cost * 1.02 / px, min_order_amt)
113
+ min_order_amount = self._amount_to_precision(
114
+ symbol, min_order_amount, mode="ceil"
115
+ )
116
+ return min_order_amount
117
+
118
+ def _amount_to_precision(
119
+ self,
120
+ symbol: str,
121
+ amount: float,
122
+ mode: Literal["round", "ceil", "floor"] = "round",
123
+ ) -> Decimal:
124
+ market = self._market[symbol]
125
+ if market.spot:
126
+ return super()._amount_to_precision(symbol, amount, mode)
127
+ else:
128
+ amount: Decimal = Decimal(str(amount))
129
+ amount_multiplier = Decimal(market.info.sizeMultiplier or "1")
130
+ multiplier_count = amount / amount_multiplier
131
+
132
+ if mode == "round":
133
+ amount = (
134
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
135
+ ) * amount_multiplier
136
+ elif mode == "ceil":
137
+ amount = (
138
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_CEILING)
139
+ ) * amount_multiplier
140
+ elif mode == "floor":
141
+ amount = (
142
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_FLOOR)
143
+ ) * amount_multiplier
144
+ return amount
145
+
146
+ def _price_to_precision(
147
+ self,
148
+ symbol: str,
149
+ price: float,
150
+ mode: Literal["round", "ceil", "floor"] = "round",
151
+ ) -> Decimal:
152
+ market = self._market[symbol]
153
+ if market.spot:
154
+ return super()._price_to_precision(symbol, price, mode)
155
+ else:
156
+ price: Decimal = Decimal(str(price))
157
+ price_multiplier = Decimal(market.info.priceEndStep or "1")
158
+ multiplier_count = price / price_multiplier
159
+
160
+ if mode == "round":
161
+ price = (
162
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
163
+ ) * price_multiplier
164
+ elif mode == "ceil":
165
+ price = (
166
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_CEILING)
167
+ ) * price_multiplier
168
+ elif mode == "floor":
169
+ price = (
170
+ multiplier_count.quantize(Decimal("1"), rounding=ROUND_FLOOR)
171
+ ) * price_multiplier
172
+ return price
173
+
174
+ async def _cancel_all_orders(
175
+ self, order_submit: CancelAllOrderSubmit, account_type: AccountType
176
+ ) -> None:
177
+ # override the base method
178
+ if isinstance(account_type, BitgetAccountType) and account_type.is_uta:
179
+ await super()._cancel_all_orders(order_submit, account_type)
180
+ else:
181
+ symbol = order_submit.symbol
182
+ await self._cache.wait_for_inflight_orders(symbol)
183
+ oids = self._cache.get_open_orders(symbol)
184
+ for oid in oids:
185
+ cancel_submit = CancelOrderSubmit(
186
+ symbol=symbol,
187
+ instrument_id=InstrumentId.from_str(symbol),
188
+ oid=oid,
189
+ )
190
+ await self._cancel_order_ws(cancel_submit, account_type)
191
+
192
+ def _get_max_order_amount(
193
+ self, symbol: str, market: BaseMarket, is_market: bool, px: float
194
+ ) -> Decimal:
195
+ if market.spot:
196
+ return Decimal("Infinity")
197
+ else:
198
+ bybit_market = cast(BitgetMarket, market)
199
+ if is_market:
200
+ return Decimal(bybit_market.info.maxMarketOrderQty or "Infinity")
201
+ else:
202
+ return Decimal(bybit_market.info.maxOrderQty or "Infinity")
@@ -0,0 +1,36 @@
1
+ class BitgetError(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 BitgetRateLimitError(Exception):
22
+ """
23
+ Raised when Bitget 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