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,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
|