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.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- 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
|