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,1195 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from walrasquant.error import PositionModeError
|
|
6
|
+
from walrasquant.exchange.bybit.error import BybitRateLimitError, BybitError
|
|
7
|
+
|
|
8
|
+
from walrasquant.core.nautilius_core import LiveClock, MessageBus
|
|
9
|
+
from walrasquant.base import OrderManagementSystem
|
|
10
|
+
from walrasquant.core.entity import TaskManager
|
|
11
|
+
from walrasquant.core.registry import OrderRegistry
|
|
12
|
+
from walrasquant.core.cache import AsyncCache
|
|
13
|
+
from walrasquant.schema import (
|
|
14
|
+
Order,
|
|
15
|
+
Position,
|
|
16
|
+
BatchOrderSubmit,
|
|
17
|
+
BaseMarket,
|
|
18
|
+
CancelOrderSubmit,
|
|
19
|
+
)
|
|
20
|
+
from walrasquant.constants import (
|
|
21
|
+
OrderSide,
|
|
22
|
+
OrderStatus,
|
|
23
|
+
ExchangeType,
|
|
24
|
+
OrderType,
|
|
25
|
+
TimeInForce,
|
|
26
|
+
PositionSide,
|
|
27
|
+
TriggerType,
|
|
28
|
+
)
|
|
29
|
+
from walrasquant.config import OrderQueryConfig
|
|
30
|
+
from walrasquant.exchange.bybit.schema import (
|
|
31
|
+
BybitOrder,
|
|
32
|
+
BybitWsMessageGeneral,
|
|
33
|
+
BybitWsApiGeneralMsg,
|
|
34
|
+
BybitWsOrderMsg,
|
|
35
|
+
BybitWsPositionMsg,
|
|
36
|
+
BybitWsAccountWalletMsg,
|
|
37
|
+
BybitWalletBalanceResponse,
|
|
38
|
+
BybitPositionStruct,
|
|
39
|
+
BybitWsApiOrderMsg,
|
|
40
|
+
)
|
|
41
|
+
from walrasquant.exchange.bybit.rest_api import BybitApiClient
|
|
42
|
+
from walrasquant.exchange.bybit.websockets import BybitWSClient, BybitWSApiClient
|
|
43
|
+
from walrasquant.exchange.bybit.constants import (
|
|
44
|
+
BybitAccountType,
|
|
45
|
+
BybitEnumParser,
|
|
46
|
+
BybitProductType,
|
|
47
|
+
BybitOrderType,
|
|
48
|
+
BybitTimeInForce,
|
|
49
|
+
BybitPositionIdx,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BybitOrderManagementSystem(OrderManagementSystem):
|
|
54
|
+
_ws_client: BybitWSClient
|
|
55
|
+
_account_type: BybitAccountType
|
|
56
|
+
_market: Dict[str, BaseMarket]
|
|
57
|
+
_market_id: Dict[str, str]
|
|
58
|
+
_api_client: BybitApiClient
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
account_type: BybitAccountType,
|
|
63
|
+
api_key: str,
|
|
64
|
+
secret: str,
|
|
65
|
+
market: Dict[str, BaseMarket],
|
|
66
|
+
market_id: Dict[str, str],
|
|
67
|
+
registry: OrderRegistry,
|
|
68
|
+
cache: AsyncCache,
|
|
69
|
+
api_client: BybitApiClient,
|
|
70
|
+
exchange_id: ExchangeType,
|
|
71
|
+
clock: LiveClock,
|
|
72
|
+
msgbus: MessageBus,
|
|
73
|
+
task_manager: TaskManager,
|
|
74
|
+
enable_rate_limit: bool,
|
|
75
|
+
order_query_config: OrderQueryConfig,
|
|
76
|
+
max_subscriptions_per_client: int | None = None,
|
|
77
|
+
max_clients: int | None = None,
|
|
78
|
+
):
|
|
79
|
+
super().__init__(
|
|
80
|
+
account_type=account_type,
|
|
81
|
+
market=market,
|
|
82
|
+
market_id=market_id,
|
|
83
|
+
registry=registry,
|
|
84
|
+
cache=cache,
|
|
85
|
+
api_client=api_client,
|
|
86
|
+
ws_client=BybitWSClient(
|
|
87
|
+
account_type=account_type,
|
|
88
|
+
handler=self._ws_msg_handler,
|
|
89
|
+
task_manager=task_manager,
|
|
90
|
+
clock=clock,
|
|
91
|
+
api_key=api_key,
|
|
92
|
+
secret=secret,
|
|
93
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
94
|
+
max_clients=max_clients,
|
|
95
|
+
),
|
|
96
|
+
exchange_id=exchange_id,
|
|
97
|
+
clock=clock,
|
|
98
|
+
msgbus=msgbus,
|
|
99
|
+
task_manager=task_manager,
|
|
100
|
+
order_query_config=order_query_config,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self._ws_api_client = BybitWSApiClient(
|
|
104
|
+
account_type=account_type,
|
|
105
|
+
api_key=api_key,
|
|
106
|
+
secret=secret,
|
|
107
|
+
handler=self._ws_api_msg_handler,
|
|
108
|
+
task_manager=task_manager,
|
|
109
|
+
clock=clock,
|
|
110
|
+
enable_rate_limit=enable_rate_limit,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._ws_api_msg_general_decoder = msgspec.json.Decoder(BybitWsApiGeneralMsg)
|
|
114
|
+
self._ws_api_msg_order_decoder = msgspec.json.Decoder(BybitWsApiOrderMsg)
|
|
115
|
+
self._ws_msg_general_decoder = msgspec.json.Decoder(BybitWsMessageGeneral)
|
|
116
|
+
self._ws_msg_order_update_decoder = msgspec.json.Decoder(BybitWsOrderMsg)
|
|
117
|
+
self._ws_msg_position_decoder = msgspec.json.Decoder(BybitWsPositionMsg)
|
|
118
|
+
self._ws_msg_wallet_decoder = msgspec.json.Decoder(BybitWsAccountWalletMsg)
|
|
119
|
+
|
|
120
|
+
def _parse_order_create(self, raw: bytes):
|
|
121
|
+
msg = self._ws_api_msg_order_decoder.decode(raw)
|
|
122
|
+
oid = msg.oid
|
|
123
|
+
|
|
124
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
125
|
+
if not tmp_order:
|
|
126
|
+
return
|
|
127
|
+
symbol = tmp_order.symbol
|
|
128
|
+
amount = tmp_order.amount
|
|
129
|
+
type = tmp_order.type
|
|
130
|
+
price = tmp_order.price
|
|
131
|
+
side = tmp_order.side
|
|
132
|
+
time_in_force = tmp_order.time_in_force
|
|
133
|
+
reduce_only = tmp_order.reduce_only
|
|
134
|
+
ts = self._clock.timestamp_ms()
|
|
135
|
+
if msg.is_success:
|
|
136
|
+
data = msg.data
|
|
137
|
+
if data is None or data.orderId is None:
|
|
138
|
+
raise ValueError("Missing Bybit order create response data")
|
|
139
|
+
ordId = data.orderId
|
|
140
|
+
self._log.debug(f"[{symbol}] new order success: oid: {oid} eid: {ordId}")
|
|
141
|
+
order = Order(
|
|
142
|
+
exchange=self._exchange_id,
|
|
143
|
+
symbol=symbol,
|
|
144
|
+
oid=oid,
|
|
145
|
+
eid=ordId,
|
|
146
|
+
side=side,
|
|
147
|
+
status=OrderStatus.PENDING,
|
|
148
|
+
amount=amount,
|
|
149
|
+
timestamp=ts,
|
|
150
|
+
type=type,
|
|
151
|
+
price=price,
|
|
152
|
+
time_in_force=time_in_force,
|
|
153
|
+
reduce_only=reduce_only,
|
|
154
|
+
)
|
|
155
|
+
self.order_status_update(order) # INITIALIZED -> PENDING
|
|
156
|
+
else:
|
|
157
|
+
self._log.error(f"[{symbol}] new order failed: oid: {oid} {msg.error_msg}")
|
|
158
|
+
order = Order(
|
|
159
|
+
exchange=self._exchange_id,
|
|
160
|
+
symbol=symbol,
|
|
161
|
+
oid=oid,
|
|
162
|
+
status=OrderStatus.FAILED,
|
|
163
|
+
amount=amount,
|
|
164
|
+
timestamp=ts,
|
|
165
|
+
side=side,
|
|
166
|
+
type=type,
|
|
167
|
+
price=price,
|
|
168
|
+
time_in_force=time_in_force,
|
|
169
|
+
reduce_only=reduce_only,
|
|
170
|
+
reason=msg.error_msg,
|
|
171
|
+
)
|
|
172
|
+
self.order_status_update(order)
|
|
173
|
+
|
|
174
|
+
def _parse_order_cancel(self, raw: bytes):
|
|
175
|
+
msg = self._ws_api_msg_order_decoder.decode(raw)
|
|
176
|
+
oid = msg.oid
|
|
177
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
178
|
+
if not tmp_order:
|
|
179
|
+
return
|
|
180
|
+
symbol = tmp_order.symbol
|
|
181
|
+
amount = tmp_order.amount
|
|
182
|
+
side = tmp_order.side
|
|
183
|
+
type = tmp_order.type
|
|
184
|
+
price = tmp_order.price
|
|
185
|
+
time_in_force = tmp_order.time_in_force
|
|
186
|
+
reduce_only = tmp_order.reduce_only
|
|
187
|
+
ts = self._clock.timestamp_ms()
|
|
188
|
+
if msg.is_success:
|
|
189
|
+
data = msg.data
|
|
190
|
+
if data is None or data.orderId is None:
|
|
191
|
+
raise ValueError("Missing Bybit order cancel response data")
|
|
192
|
+
ordId = data.orderId
|
|
193
|
+
self._log.debug(
|
|
194
|
+
f"[{symbol}] canceling order success: oid: {oid} id: {ordId}"
|
|
195
|
+
)
|
|
196
|
+
order = Order(
|
|
197
|
+
exchange=self._exchange_id,
|
|
198
|
+
symbol=symbol,
|
|
199
|
+
oid=oid,
|
|
200
|
+
eid=ordId,
|
|
201
|
+
side=side,
|
|
202
|
+
status=OrderStatus.CANCELING,
|
|
203
|
+
amount=amount,
|
|
204
|
+
type=type,
|
|
205
|
+
price=price,
|
|
206
|
+
time_in_force=time_in_force,
|
|
207
|
+
timestamp=ts,
|
|
208
|
+
reduce_only=reduce_only,
|
|
209
|
+
)
|
|
210
|
+
self.order_status_update(order)
|
|
211
|
+
else:
|
|
212
|
+
self._log.error(
|
|
213
|
+
f"[{symbol}] canceling order failed: oid: {oid} {msg.error_msg}"
|
|
214
|
+
)
|
|
215
|
+
order = Order(
|
|
216
|
+
exchange=self._exchange_id,
|
|
217
|
+
symbol=symbol,
|
|
218
|
+
oid=oid,
|
|
219
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
220
|
+
amount=amount,
|
|
221
|
+
side=side,
|
|
222
|
+
type=type,
|
|
223
|
+
price=price,
|
|
224
|
+
time_in_force=time_in_force,
|
|
225
|
+
timestamp=ts,
|
|
226
|
+
reduce_only=reduce_only,
|
|
227
|
+
reason=msg.error_msg,
|
|
228
|
+
)
|
|
229
|
+
self.order_status_update(order)
|
|
230
|
+
|
|
231
|
+
def _ws_api_msg_handler(self, raw: bytes):
|
|
232
|
+
try:
|
|
233
|
+
ws_msg = self._ws_api_msg_general_decoder.decode(raw)
|
|
234
|
+
# if ws_msg.is_pong:
|
|
235
|
+
# self._ws_api_client._transport.notify_user_specific_pong_received()
|
|
236
|
+
# self._log.debug(f"Pong received {str(ws_msg)}")
|
|
237
|
+
# return
|
|
238
|
+
if ws_msg.is_order_create:
|
|
239
|
+
self._parse_order_create(raw)
|
|
240
|
+
elif ws_msg.is_order_cancel:
|
|
241
|
+
self._parse_order_cancel(raw)
|
|
242
|
+
except msgspec.DecodeError as e:
|
|
243
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
244
|
+
|
|
245
|
+
def _ws_msg_handler(self, raw: bytes):
|
|
246
|
+
try:
|
|
247
|
+
ws_msg = self._ws_msg_general_decoder.decode(raw)
|
|
248
|
+
# if ws_msg.op == "pong":
|
|
249
|
+
# self._ws_client._transport.notify_user_specific_pong_received()
|
|
250
|
+
# self._log.debug(f"Pong received {str(ws_msg)}")
|
|
251
|
+
# return
|
|
252
|
+
if ws_msg.success is False:
|
|
253
|
+
self._log.error(f"WebSocket error: {ws_msg}")
|
|
254
|
+
return
|
|
255
|
+
if "order" in ws_msg.topic:
|
|
256
|
+
self._parse_order_update(raw)
|
|
257
|
+
elif "position" in ws_msg.topic:
|
|
258
|
+
self._parse_position_update(raw)
|
|
259
|
+
elif "wallet" == ws_msg.topic:
|
|
260
|
+
self._parse_wallet_update(raw)
|
|
261
|
+
except msgspec.DecodeError as e:
|
|
262
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
263
|
+
|
|
264
|
+
def _get_category(self, market: BaseMarket):
|
|
265
|
+
if market.spot:
|
|
266
|
+
return "spot"
|
|
267
|
+
elif market.linear:
|
|
268
|
+
return "linear"
|
|
269
|
+
elif market.inverse:
|
|
270
|
+
return "inverse"
|
|
271
|
+
else:
|
|
272
|
+
raise ValueError(f"Unsupported market type: {market.type}")
|
|
273
|
+
|
|
274
|
+
async def cancel_order_ws(self, oid: str, symbol: str, **kwargs):
|
|
275
|
+
market = self._market.get(symbol)
|
|
276
|
+
if not market:
|
|
277
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
278
|
+
id = market.id
|
|
279
|
+
category = self._get_category(market)
|
|
280
|
+
params = {
|
|
281
|
+
"category": category,
|
|
282
|
+
"symbol": id,
|
|
283
|
+
"orderLinkId": oid,
|
|
284
|
+
**kwargs,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await self._ws_api_client.cancel_order(id=oid, **params)
|
|
288
|
+
|
|
289
|
+
async def cancel_order(self, oid: str, symbol: str, **kwargs):
|
|
290
|
+
market = self._market.get(symbol)
|
|
291
|
+
if not market:
|
|
292
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
293
|
+
id = market.id
|
|
294
|
+
category = self._get_category(market)
|
|
295
|
+
params = {
|
|
296
|
+
"category": category,
|
|
297
|
+
"symbol": id,
|
|
298
|
+
"orderLinkId": oid,
|
|
299
|
+
**kwargs,
|
|
300
|
+
}
|
|
301
|
+
try:
|
|
302
|
+
res = await self._api_client.post_v5_order_cancel(**params)
|
|
303
|
+
order = Order(
|
|
304
|
+
oid=oid,
|
|
305
|
+
eid=res.result.orderId,
|
|
306
|
+
exchange=self._exchange_id,
|
|
307
|
+
timestamp=res.time,
|
|
308
|
+
symbol=symbol,
|
|
309
|
+
status=OrderStatus.CANCELING,
|
|
310
|
+
)
|
|
311
|
+
except BybitRateLimitError as e:
|
|
312
|
+
error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s): {str(e)}"
|
|
313
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
314
|
+
order = Order(
|
|
315
|
+
oid=oid,
|
|
316
|
+
exchange=self._exchange_id,
|
|
317
|
+
timestamp=self._clock.timestamp_ms(),
|
|
318
|
+
symbol=symbol,
|
|
319
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
320
|
+
reason=error_msg,
|
|
321
|
+
)
|
|
322
|
+
except BybitError as e:
|
|
323
|
+
error_msg = str(e)
|
|
324
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
325
|
+
order = Order(
|
|
326
|
+
oid=oid,
|
|
327
|
+
exchange=self._exchange_id,
|
|
328
|
+
timestamp=self._clock.timestamp_ms(),
|
|
329
|
+
symbol=symbol,
|
|
330
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
331
|
+
reason=error_msg,
|
|
332
|
+
)
|
|
333
|
+
except Exception as e:
|
|
334
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
335
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
336
|
+
order = Order(
|
|
337
|
+
oid=oid,
|
|
338
|
+
exchange=self._exchange_id,
|
|
339
|
+
timestamp=self._clock.timestamp_ms(),
|
|
340
|
+
symbol=symbol,
|
|
341
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
342
|
+
reason=error_msg,
|
|
343
|
+
)
|
|
344
|
+
self.order_status_update(order)
|
|
345
|
+
|
|
346
|
+
async def _init_account_balance(self):
|
|
347
|
+
res: BybitWalletBalanceResponse = (
|
|
348
|
+
await self._api_client.get_v5_account_wallet_balance(account_type="UNIFIED")
|
|
349
|
+
)
|
|
350
|
+
for result in res.result.list:
|
|
351
|
+
self._cache._apply_balance(self._account_type, result.parse_to_balances())
|
|
352
|
+
|
|
353
|
+
async def _get_all_positions_list(
|
|
354
|
+
self, category: BybitProductType, settle_coin: str | None = None
|
|
355
|
+
) -> list[BybitPositionStruct]:
|
|
356
|
+
all_positions = []
|
|
357
|
+
next_page_cursor = ""
|
|
358
|
+
|
|
359
|
+
while True:
|
|
360
|
+
res = await self._api_client.get_v5_position_list(
|
|
361
|
+
category=category.value,
|
|
362
|
+
settleCoin=settle_coin,
|
|
363
|
+
limit=200,
|
|
364
|
+
cursor=next_page_cursor,
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
all_positions.extend(res.result.list)
|
|
368
|
+
|
|
369
|
+
# If there's no next page cursor, we've reached the end
|
|
370
|
+
if not res.result.nextPageCursor:
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
next_page_cursor = res.result.nextPageCursor
|
|
374
|
+
|
|
375
|
+
return all_positions
|
|
376
|
+
|
|
377
|
+
async def _init_position(self):
|
|
378
|
+
res_linear_usdt = await self._get_all_positions_list(
|
|
379
|
+
BybitProductType.LINEAR, settle_coin="USDT"
|
|
380
|
+
)
|
|
381
|
+
res_linear_usdc = await self._get_all_positions_list(
|
|
382
|
+
BybitProductType.LINEAR, settle_coin="USDC"
|
|
383
|
+
)
|
|
384
|
+
res_inverse = await self._get_all_positions_list(BybitProductType.INVERSE)
|
|
385
|
+
|
|
386
|
+
self._apply_cache_position(res_linear_usdt, BybitProductType.LINEAR)
|
|
387
|
+
self._apply_cache_position(res_linear_usdc, BybitProductType.LINEAR)
|
|
388
|
+
self._apply_cache_position(res_inverse, BybitProductType.INVERSE)
|
|
389
|
+
|
|
390
|
+
self._cache.get_all_positions()
|
|
391
|
+
|
|
392
|
+
async def _position_mode_check(self):
|
|
393
|
+
# NOTE: no need to implement this for bybit, we do position mode check in _get_all_positions_list
|
|
394
|
+
pass
|
|
395
|
+
|
|
396
|
+
def _apply_cache_position(
|
|
397
|
+
self, positions: list[BybitPositionStruct], category: BybitProductType
|
|
398
|
+
):
|
|
399
|
+
for result in positions:
|
|
400
|
+
side = result.side.parse_to_position_side()
|
|
401
|
+
if side == PositionSide.FLAT:
|
|
402
|
+
signed_amount = Decimal(0)
|
|
403
|
+
side = None
|
|
404
|
+
elif side == PositionSide.LONG:
|
|
405
|
+
signed_amount = Decimal(result.size)
|
|
406
|
+
elif side == PositionSide.SHORT:
|
|
407
|
+
signed_amount = -Decimal(result.size)
|
|
408
|
+
|
|
409
|
+
if category.is_inverse:
|
|
410
|
+
market_id = result.symbol + "_inverse"
|
|
411
|
+
elif category.is_linear:
|
|
412
|
+
market_id = result.symbol + "_linear"
|
|
413
|
+
else:
|
|
414
|
+
raise ValueError(f"Unsupported position category: {category}")
|
|
415
|
+
|
|
416
|
+
symbol = self._market_id[market_id]
|
|
417
|
+
|
|
418
|
+
if not result.positionIdx.is_one_way_mode():
|
|
419
|
+
raise PositionModeError(
|
|
420
|
+
f"Please Set Position Mode to `One-Way Mode` in Bybit App for {symbol}"
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
position = Position(
|
|
424
|
+
symbol=symbol,
|
|
425
|
+
exchange=self._exchange_id,
|
|
426
|
+
side=side,
|
|
427
|
+
signed_amount=signed_amount,
|
|
428
|
+
entry_price=float(result.avgPrice),
|
|
429
|
+
unrealized_pnl=float(result.unrealisedPnl),
|
|
430
|
+
realized_pnl=float(result.cumRealisedPnl),
|
|
431
|
+
)
|
|
432
|
+
self._cache._apply_position(position)
|
|
433
|
+
|
|
434
|
+
async def create_tp_sl_order(
|
|
435
|
+
self,
|
|
436
|
+
oid: str,
|
|
437
|
+
symbol: str,
|
|
438
|
+
side: OrderSide,
|
|
439
|
+
type: OrderType,
|
|
440
|
+
amount: Decimal,
|
|
441
|
+
price: Decimal | None = None,
|
|
442
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
443
|
+
tp_order_type: OrderType | None = None,
|
|
444
|
+
tp_trigger_price: Decimal | None = None,
|
|
445
|
+
tp_price: Decimal | None = None,
|
|
446
|
+
tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
|
|
447
|
+
sl_order_type: OrderType | None = None,
|
|
448
|
+
sl_trigger_price: Decimal | None = None,
|
|
449
|
+
sl_price: Decimal | None = None,
|
|
450
|
+
sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
|
|
451
|
+
**kwargs,
|
|
452
|
+
) -> Order:
|
|
453
|
+
"""Create a take profit and stop loss order"""
|
|
454
|
+
market = self._market.get(symbol)
|
|
455
|
+
if not market:
|
|
456
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
457
|
+
id = market.id
|
|
458
|
+
|
|
459
|
+
category = self._get_category(market)
|
|
460
|
+
|
|
461
|
+
tif = time_in_force or TimeInForce.GTC
|
|
462
|
+
tif = time_in_force or TimeInForce.GTC
|
|
463
|
+
params: dict[str, object] = {
|
|
464
|
+
"category": category,
|
|
465
|
+
"symbol": id,
|
|
466
|
+
"side": BybitEnumParser.to_bybit_order_side(side).value,
|
|
467
|
+
"qty": amount,
|
|
468
|
+
"orderLinkId": oid,
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if type.is_limit:
|
|
472
|
+
if not price:
|
|
473
|
+
raise ValueError("Price is required for limit order")
|
|
474
|
+
params["price"] = str(price)
|
|
475
|
+
params["order_type"] = BybitOrderType.LIMIT.value
|
|
476
|
+
params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(tif).value
|
|
477
|
+
elif type.is_post_only:
|
|
478
|
+
if not price:
|
|
479
|
+
raise ValueError("Price is required for post-only order")
|
|
480
|
+
params["order_type"] = BybitOrderType.LIMIT.value
|
|
481
|
+
params["price"] = str(price)
|
|
482
|
+
params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
|
|
483
|
+
elif type == OrderType.MARKET:
|
|
484
|
+
params["order_type"] = BybitOrderType.MARKET.value
|
|
485
|
+
|
|
486
|
+
if market.spot:
|
|
487
|
+
params["marketUnit"] = "baseCoin"
|
|
488
|
+
|
|
489
|
+
if tp_order_type:
|
|
490
|
+
params["takeProfit"] = str(tp_trigger_price)
|
|
491
|
+
params["triggerBy"] = BybitEnumParser.to_bybit_trigger_type(
|
|
492
|
+
tp_trigger_type or TriggerType.LAST_PRICE
|
|
493
|
+
).value
|
|
494
|
+
if tp_order_type.is_limit:
|
|
495
|
+
if not tp_price:
|
|
496
|
+
raise ValueError("Price is required for limit take profit order")
|
|
497
|
+
params["tpOrderType"] = BybitOrderType.LIMIT.value
|
|
498
|
+
params["tpLimitPrice"] = str(tp_price)
|
|
499
|
+
|
|
500
|
+
if sl_order_type:
|
|
501
|
+
params["stopLoss"] = str(sl_trigger_price)
|
|
502
|
+
params["triggerBy"] = BybitEnumParser.to_bybit_trigger_type(
|
|
503
|
+
sl_trigger_type or TriggerType.LAST_PRICE
|
|
504
|
+
).value
|
|
505
|
+
if sl_order_type.is_limit:
|
|
506
|
+
if not sl_price:
|
|
507
|
+
raise ValueError("Price is required for limit stop loss order")
|
|
508
|
+
params["slOrderType"] = BybitOrderType.LIMIT.value
|
|
509
|
+
params["slLimitPrice"] = str(sl_price)
|
|
510
|
+
|
|
511
|
+
if not market.spot:
|
|
512
|
+
tpslMode = kwargs.pop("tpslMode", "Partial")
|
|
513
|
+
params["tpslMode"] = tpslMode
|
|
514
|
+
|
|
515
|
+
params.update(kwargs)
|
|
516
|
+
|
|
517
|
+
try:
|
|
518
|
+
res = await self._api_client.post_v5_order_create(
|
|
519
|
+
category=category,
|
|
520
|
+
symbol=id,
|
|
521
|
+
side=BybitEnumParser.to_bybit_order_side(side).value,
|
|
522
|
+
order_type=str(params["order_type"]),
|
|
523
|
+
qty=amount,
|
|
524
|
+
**{
|
|
525
|
+
k: v
|
|
526
|
+
for k, v in params.items()
|
|
527
|
+
if k not in {"category", "symbol", "side", "order_type", "qty"}
|
|
528
|
+
},
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
order = Order(
|
|
532
|
+
exchange=self._exchange_id,
|
|
533
|
+
eid=res.result.orderId,
|
|
534
|
+
oid=oid,
|
|
535
|
+
timestamp=int(res.time),
|
|
536
|
+
symbol=symbol,
|
|
537
|
+
type=type,
|
|
538
|
+
side=side,
|
|
539
|
+
amount=amount,
|
|
540
|
+
price=float(price) if price else None,
|
|
541
|
+
time_in_force=time_in_force,
|
|
542
|
+
status=OrderStatus.PENDING,
|
|
543
|
+
filled=Decimal(0),
|
|
544
|
+
remaining=amount,
|
|
545
|
+
reduce_only=False,
|
|
546
|
+
)
|
|
547
|
+
return order
|
|
548
|
+
except Exception as e:
|
|
549
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
550
|
+
self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
|
|
551
|
+
order = Order(
|
|
552
|
+
exchange=self._exchange_id,
|
|
553
|
+
timestamp=self._clock.timestamp_ms(),
|
|
554
|
+
symbol=symbol,
|
|
555
|
+
oid=oid,
|
|
556
|
+
type=type,
|
|
557
|
+
side=side,
|
|
558
|
+
amount=amount,
|
|
559
|
+
price=float(price) if price else None,
|
|
560
|
+
time_in_force=time_in_force,
|
|
561
|
+
status=OrderStatus.FAILED,
|
|
562
|
+
filled=Decimal(0),
|
|
563
|
+
remaining=amount,
|
|
564
|
+
reason=error_msg,
|
|
565
|
+
)
|
|
566
|
+
return order
|
|
567
|
+
|
|
568
|
+
async def create_batch_orders(self, orders: List[BatchOrderSubmit]):
|
|
569
|
+
if not orders:
|
|
570
|
+
raise ValueError("No orders provided for batch submission")
|
|
571
|
+
|
|
572
|
+
# Get category from first order
|
|
573
|
+
first_market = self._market.get(orders[0].symbol)
|
|
574
|
+
if not first_market:
|
|
575
|
+
raise ValueError(f"Symbol {orders[0].symbol} not found in market")
|
|
576
|
+
category = self._get_category(first_market)
|
|
577
|
+
|
|
578
|
+
batch_orders = []
|
|
579
|
+
for order in orders:
|
|
580
|
+
market = self._market.get(order.symbol)
|
|
581
|
+
if not market:
|
|
582
|
+
raise ValueError(f"Symbol {order.symbol} not found in market")
|
|
583
|
+
id = market.id
|
|
584
|
+
|
|
585
|
+
params: dict[str, object] = {
|
|
586
|
+
"symbol": id,
|
|
587
|
+
"side": BybitEnumParser.to_bybit_order_side(order.side).value,
|
|
588
|
+
"qty": str(order.amount),
|
|
589
|
+
"orderLinkId": order.oid,
|
|
590
|
+
}
|
|
591
|
+
if order.type.is_limit:
|
|
592
|
+
if not order.price:
|
|
593
|
+
raise ValueError("Price is required for limit order")
|
|
594
|
+
params["orderType"] = BybitOrderType.LIMIT.value
|
|
595
|
+
params["price"] = str(order.price)
|
|
596
|
+
params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(
|
|
597
|
+
order.time_in_force
|
|
598
|
+
).value
|
|
599
|
+
elif order.type.is_post_only:
|
|
600
|
+
if not order.price:
|
|
601
|
+
raise ValueError("Price is required for limit order")
|
|
602
|
+
params["orderType"] = BybitOrderType.LIMIT.value
|
|
603
|
+
params["price"] = str(order.price)
|
|
604
|
+
params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
|
|
605
|
+
elif order.type == OrderType.MARKET:
|
|
606
|
+
params["orderType"] = BybitOrderType.MARKET.value
|
|
607
|
+
|
|
608
|
+
if order.reduce_only:
|
|
609
|
+
params["reduceOnly"] = True
|
|
610
|
+
if market.spot:
|
|
611
|
+
params["marketUnit"] = "baseCoin"
|
|
612
|
+
params.update(order.kwargs)
|
|
613
|
+
batch_orders.append(params)
|
|
614
|
+
|
|
615
|
+
try:
|
|
616
|
+
res = await self._api_client.post_v5_order_create_batch(
|
|
617
|
+
category=category, request=batch_orders
|
|
618
|
+
)
|
|
619
|
+
for order, res_order, res_ext in zip(
|
|
620
|
+
orders, res.result.list, res.retExtInfo.list
|
|
621
|
+
):
|
|
622
|
+
if res_ext.code == 0:
|
|
623
|
+
res_batch_order = Order(
|
|
624
|
+
exchange=self._exchange_id,
|
|
625
|
+
oid=order.oid,
|
|
626
|
+
eid=res_order.orderId,
|
|
627
|
+
timestamp=int(res_order.createAt),
|
|
628
|
+
symbol=order.symbol,
|
|
629
|
+
type=order.type,
|
|
630
|
+
side=order.side,
|
|
631
|
+
amount=order.amount,
|
|
632
|
+
price=float(order.price) if order.price else None,
|
|
633
|
+
time_in_force=order.time_in_force,
|
|
634
|
+
status=OrderStatus.PENDING,
|
|
635
|
+
filled=Decimal(0),
|
|
636
|
+
remaining=order.amount,
|
|
637
|
+
reduce_only=order.reduce_only,
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
res_batch_order = Order(
|
|
641
|
+
exchange=self._exchange_id,
|
|
642
|
+
timestamp=self._clock.timestamp_ms(),
|
|
643
|
+
symbol=order.symbol,
|
|
644
|
+
type=order.type,
|
|
645
|
+
oid=order.oid,
|
|
646
|
+
side=order.side,
|
|
647
|
+
amount=order.amount,
|
|
648
|
+
price=float(order.price) if order.price else None,
|
|
649
|
+
time_in_force=order.time_in_force,
|
|
650
|
+
status=OrderStatus.FAILED,
|
|
651
|
+
filled=Decimal(0),
|
|
652
|
+
remaining=order.amount,
|
|
653
|
+
reduce_only=order.reduce_only,
|
|
654
|
+
reason=f"code={res_ext.code}, msg={res_ext.msg}",
|
|
655
|
+
)
|
|
656
|
+
self._log.error(
|
|
657
|
+
f"Failed to place order for {order.symbol}: {res_ext.msg} code: {res_ext.code} {order.oid}"
|
|
658
|
+
)
|
|
659
|
+
self.order_status_update(res_batch_order)
|
|
660
|
+
except Exception as e:
|
|
661
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
662
|
+
self._log.error(f"Error creating batch orders: {error_msg}")
|
|
663
|
+
for order in orders:
|
|
664
|
+
res_batch_order = Order(
|
|
665
|
+
exchange=self._exchange_id,
|
|
666
|
+
timestamp=self._clock.timestamp_ms(),
|
|
667
|
+
symbol=order.symbol,
|
|
668
|
+
oid=order.oid,
|
|
669
|
+
type=order.type,
|
|
670
|
+
side=order.side,
|
|
671
|
+
amount=order.amount,
|
|
672
|
+
price=float(order.price) if order.price else None,
|
|
673
|
+
time_in_force=order.time_in_force,
|
|
674
|
+
status=OrderStatus.FAILED,
|
|
675
|
+
filled=Decimal(0),
|
|
676
|
+
remaining=order.amount,
|
|
677
|
+
reduce_only=order.reduce_only,
|
|
678
|
+
reason=error_msg,
|
|
679
|
+
)
|
|
680
|
+
self.order_status_update(res_batch_order)
|
|
681
|
+
|
|
682
|
+
async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]):
|
|
683
|
+
from collections import defaultdict
|
|
684
|
+
|
|
685
|
+
groups = defaultdict(list)
|
|
686
|
+
for order in orders:
|
|
687
|
+
market = self._market.get(order.symbol)
|
|
688
|
+
if not market:
|
|
689
|
+
raise ValueError(f"Symbol {order.symbol} not found")
|
|
690
|
+
category = self._get_category(market)
|
|
691
|
+
groups[category].append((market, order))
|
|
692
|
+
|
|
693
|
+
for category, items in groups.items():
|
|
694
|
+
chunk_size = 10 if category == "spot" else 20
|
|
695
|
+
for i in range(0, len(items), chunk_size):
|
|
696
|
+
chunk = items[i : i + chunk_size]
|
|
697
|
+
request = [{"symbol": m.id, "orderLinkId": o.oid} for m, o in chunk]
|
|
698
|
+
try:
|
|
699
|
+
res = await self._api_client.post_v5_order_cancel_batch(
|
|
700
|
+
category=category, request=request
|
|
701
|
+
)
|
|
702
|
+
for (_, submit), res_order, res_ext in zip(
|
|
703
|
+
chunk, res.result.list, res.retExtInfo.list
|
|
704
|
+
):
|
|
705
|
+
if res_ext.code == 0:
|
|
706
|
+
order = Order(
|
|
707
|
+
exchange=self._exchange_id,
|
|
708
|
+
symbol=submit.symbol,
|
|
709
|
+
oid=res_order.orderLinkId,
|
|
710
|
+
eid=res_order.orderId,
|
|
711
|
+
timestamp=res.time,
|
|
712
|
+
status=OrderStatus.CANCELING,
|
|
713
|
+
)
|
|
714
|
+
else:
|
|
715
|
+
order = Order(
|
|
716
|
+
exchange=self._exchange_id,
|
|
717
|
+
symbol=submit.symbol,
|
|
718
|
+
oid=submit.oid,
|
|
719
|
+
timestamp=self._clock.timestamp_ms(),
|
|
720
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
721
|
+
reason=f"code={res_ext.code}, msg={res_ext.msg}",
|
|
722
|
+
)
|
|
723
|
+
self._log.error(
|
|
724
|
+
f"Batch cancel failed for {submit.symbol} oid={submit.oid}: {res_ext.msg}"
|
|
725
|
+
)
|
|
726
|
+
self.order_status_update(order)
|
|
727
|
+
except Exception as e:
|
|
728
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
729
|
+
self._log.error(f"Error canceling batch orders: {error_msg}")
|
|
730
|
+
for _, submit in chunk:
|
|
731
|
+
order = Order(
|
|
732
|
+
exchange=self._exchange_id,
|
|
733
|
+
symbol=submit.symbol,
|
|
734
|
+
oid=submit.oid,
|
|
735
|
+
timestamp=self._clock.timestamp_ms(),
|
|
736
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
737
|
+
reason=error_msg,
|
|
738
|
+
)
|
|
739
|
+
self.order_status_update(order)
|
|
740
|
+
|
|
741
|
+
async def create_order(
|
|
742
|
+
self,
|
|
743
|
+
oid: str,
|
|
744
|
+
symbol: str,
|
|
745
|
+
side: OrderSide,
|
|
746
|
+
type: OrderType,
|
|
747
|
+
amount: Decimal,
|
|
748
|
+
price: Decimal | None = None,
|
|
749
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
750
|
+
reduce_only: bool = False,
|
|
751
|
+
**kwargs,
|
|
752
|
+
):
|
|
753
|
+
market = self._market.get(symbol)
|
|
754
|
+
if not market:
|
|
755
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
756
|
+
id = market.id
|
|
757
|
+
|
|
758
|
+
category = self._get_category(market)
|
|
759
|
+
tif = time_in_force or TimeInForce.GTC
|
|
760
|
+
|
|
761
|
+
params: dict[str, object] = {
|
|
762
|
+
"category": category,
|
|
763
|
+
"symbol": id,
|
|
764
|
+
"side": BybitEnumParser.to_bybit_order_side(side).value,
|
|
765
|
+
"qty": amount,
|
|
766
|
+
"orderLinkId": oid,
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if type.is_limit:
|
|
770
|
+
if not price:
|
|
771
|
+
raise ValueError("Price is required for limit order")
|
|
772
|
+
params["price"] = str(price)
|
|
773
|
+
params["order_type"] = BybitOrderType.LIMIT.value
|
|
774
|
+
params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(tif).value
|
|
775
|
+
elif type.is_post_only:
|
|
776
|
+
if not price:
|
|
777
|
+
raise ValueError("Price is required for post-only order")
|
|
778
|
+
params["order_type"] = BybitOrderType.LIMIT.value
|
|
779
|
+
params["price"] = str(price)
|
|
780
|
+
params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
|
|
781
|
+
elif type == OrderType.MARKET:
|
|
782
|
+
params["order_type"] = BybitOrderType.MARKET.value
|
|
783
|
+
|
|
784
|
+
# if position_side:
|
|
785
|
+
# params["positionIdx"] = BybitEnumParser.to_bybit_position_side(
|
|
786
|
+
# position_side
|
|
787
|
+
# ).value
|
|
788
|
+
if reduce_only:
|
|
789
|
+
params["reduceOnly"] = True
|
|
790
|
+
if market.spot:
|
|
791
|
+
params["marketUnit"] = "baseCoin"
|
|
792
|
+
|
|
793
|
+
params.update(kwargs)
|
|
794
|
+
|
|
795
|
+
try:
|
|
796
|
+
res = await self._api_client.post_v5_order_create(
|
|
797
|
+
category=category,
|
|
798
|
+
symbol=id,
|
|
799
|
+
side=BybitEnumParser.to_bybit_order_side(side).value,
|
|
800
|
+
order_type=str(params["order_type"]),
|
|
801
|
+
qty=amount,
|
|
802
|
+
**{
|
|
803
|
+
k: v
|
|
804
|
+
for k, v in params.items()
|
|
805
|
+
if k not in {"category", "symbol", "side", "order_type", "qty"}
|
|
806
|
+
},
|
|
807
|
+
)
|
|
808
|
+
order = Order(
|
|
809
|
+
exchange=self._exchange_id,
|
|
810
|
+
eid=res.result.orderId,
|
|
811
|
+
oid=oid,
|
|
812
|
+
timestamp=int(res.time),
|
|
813
|
+
symbol=symbol,
|
|
814
|
+
type=type,
|
|
815
|
+
side=side,
|
|
816
|
+
amount=amount,
|
|
817
|
+
price=float(price) if price else None,
|
|
818
|
+
time_in_force=time_in_force,
|
|
819
|
+
# position_side=position_side,
|
|
820
|
+
status=OrderStatus.PENDING,
|
|
821
|
+
filled=Decimal(0),
|
|
822
|
+
remaining=amount,
|
|
823
|
+
reduce_only=reduce_only,
|
|
824
|
+
)
|
|
825
|
+
except Exception as e:
|
|
826
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
827
|
+
self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
|
|
828
|
+
order = Order(
|
|
829
|
+
exchange=self._exchange_id,
|
|
830
|
+
oid=oid,
|
|
831
|
+
timestamp=self._clock.timestamp_ms(),
|
|
832
|
+
symbol=symbol,
|
|
833
|
+
type=type,
|
|
834
|
+
side=side,
|
|
835
|
+
amount=amount,
|
|
836
|
+
price=float(price) if price else None,
|
|
837
|
+
time_in_force=time_in_force,
|
|
838
|
+
# position_side=position_side,
|
|
839
|
+
status=OrderStatus.FAILED,
|
|
840
|
+
filled=Decimal(0),
|
|
841
|
+
remaining=amount,
|
|
842
|
+
reduce_only=reduce_only,
|
|
843
|
+
reason=error_msg,
|
|
844
|
+
)
|
|
845
|
+
self.order_status_update(order)
|
|
846
|
+
|
|
847
|
+
async def create_order_ws(
|
|
848
|
+
self,
|
|
849
|
+
oid: str,
|
|
850
|
+
symbol: str,
|
|
851
|
+
side: OrderSide,
|
|
852
|
+
type: OrderType,
|
|
853
|
+
amount: Decimal,
|
|
854
|
+
price: Decimal | None = None,
|
|
855
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
856
|
+
reduce_only: bool = False,
|
|
857
|
+
**kwargs,
|
|
858
|
+
) -> Order:
|
|
859
|
+
self._registry.register_tmp_order(
|
|
860
|
+
order=Order(
|
|
861
|
+
oid=oid,
|
|
862
|
+
exchange=self._exchange_id,
|
|
863
|
+
symbol=symbol,
|
|
864
|
+
status=OrderStatus.INITIALIZED,
|
|
865
|
+
amount=amount,
|
|
866
|
+
type=type,
|
|
867
|
+
side=side,
|
|
868
|
+
price=float(price) if price else None,
|
|
869
|
+
time_in_force=time_in_force,
|
|
870
|
+
reduce_only=reduce_only,
|
|
871
|
+
timestamp=self._clock.timestamp_ms(),
|
|
872
|
+
)
|
|
873
|
+
)
|
|
874
|
+
market = self._market.get(symbol)
|
|
875
|
+
if not market:
|
|
876
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
877
|
+
id = market.id
|
|
878
|
+
|
|
879
|
+
category = self._get_category(market)
|
|
880
|
+
|
|
881
|
+
tif = time_in_force or TimeInForce.GTC
|
|
882
|
+
params: dict[str, object] = {
|
|
883
|
+
"category": category,
|
|
884
|
+
"symbol": id,
|
|
885
|
+
"side": BybitEnumParser.to_bybit_order_side(side).value,
|
|
886
|
+
"qty": str(amount),
|
|
887
|
+
"orderLinkId": oid,
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if type.is_limit:
|
|
891
|
+
if not price:
|
|
892
|
+
raise ValueError("Price is required for limit order")
|
|
893
|
+
params["price"] = str(price)
|
|
894
|
+
params["orderType"] = BybitOrderType.LIMIT.value
|
|
895
|
+
params["timeInForce"] = BybitEnumParser.to_bybit_time_in_force(tif).value
|
|
896
|
+
elif type.is_post_only:
|
|
897
|
+
if not price:
|
|
898
|
+
raise ValueError("Price is required for post-only order")
|
|
899
|
+
params["orderType"] = BybitOrderType.LIMIT.value
|
|
900
|
+
params["price"] = str(price)
|
|
901
|
+
params["timeInForce"] = BybitTimeInForce.POST_ONLY.value
|
|
902
|
+
elif type == OrderType.MARKET:
|
|
903
|
+
params["orderType"] = BybitOrderType.MARKET.value
|
|
904
|
+
|
|
905
|
+
# if position_side:
|
|
906
|
+
# params["positionIdx"] = BybitEnumParser.to_bybit_position_side(
|
|
907
|
+
# position_side
|
|
908
|
+
# ).value
|
|
909
|
+
if reduce_only:
|
|
910
|
+
params["reduceOnly"] = True
|
|
911
|
+
if market.spot:
|
|
912
|
+
params["marketUnit"] = "baseCoin"
|
|
913
|
+
|
|
914
|
+
params.update(kwargs)
|
|
915
|
+
|
|
916
|
+
try:
|
|
917
|
+
await self._ws_api_client.create_order(
|
|
918
|
+
id=oid,
|
|
919
|
+
category=category,
|
|
920
|
+
symbol=id,
|
|
921
|
+
side=BybitEnumParser.to_bybit_order_side(side).value,
|
|
922
|
+
orderType=str(params["orderType"]),
|
|
923
|
+
qty=str(amount),
|
|
924
|
+
**{
|
|
925
|
+
k: v
|
|
926
|
+
for k, v in params.items()
|
|
927
|
+
if k not in {"category", "symbol", "side", "orderType", "qty"}
|
|
928
|
+
},
|
|
929
|
+
)
|
|
930
|
+
except BybitRateLimitError as e:
|
|
931
|
+
order = self._rate_limit_failed_order(
|
|
932
|
+
oid=oid,
|
|
933
|
+
symbol=symbol,
|
|
934
|
+
side=side,
|
|
935
|
+
type=type,
|
|
936
|
+
amount=amount,
|
|
937
|
+
price=price,
|
|
938
|
+
time_in_force=time_in_force,
|
|
939
|
+
reduce_only=reduce_only,
|
|
940
|
+
exc=e,
|
|
941
|
+
)
|
|
942
|
+
self.order_status_update(order)
|
|
943
|
+
return order
|
|
944
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
945
|
+
if tmp_order is None:
|
|
946
|
+
raise ValueError(f"Temporary order {oid} was not registered")
|
|
947
|
+
return tmp_order
|
|
948
|
+
|
|
949
|
+
def _parse_rest_order(
|
|
950
|
+
self, data: BybitOrder, symbol: str, fallback_oid: str
|
|
951
|
+
) -> Order:
|
|
952
|
+
time_in_force = BybitEnumParser.parse_time_in_force(data.timeInForce)
|
|
953
|
+
order_type = BybitEnumParser.parse_order_type(data.orderType, time_in_force)
|
|
954
|
+
|
|
955
|
+
position_idx = (
|
|
956
|
+
data.positionIdx
|
|
957
|
+
if isinstance(data.positionIdx, BybitPositionIdx)
|
|
958
|
+
else BybitPositionIdx(data.positionIdx)
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
return Order(
|
|
962
|
+
exchange=self._exchange_id,
|
|
963
|
+
symbol=symbol,
|
|
964
|
+
status=BybitEnumParser.parse_order_status(data.orderStatus),
|
|
965
|
+
eid=data.orderId,
|
|
966
|
+
oid=data.orderLinkId or fallback_oid,
|
|
967
|
+
timestamp=int(data.updatedTime),
|
|
968
|
+
type=order_type,
|
|
969
|
+
side=BybitEnumParser.parse_order_side(data.side),
|
|
970
|
+
time_in_force=time_in_force,
|
|
971
|
+
price=float(data.price),
|
|
972
|
+
average=float(data.avgPrice) if data.avgPrice else None,
|
|
973
|
+
amount=Decimal(data.qty),
|
|
974
|
+
filled=Decimal(data.cumExecQty),
|
|
975
|
+
remaining=Decimal(data.qty) - Decimal(data.cumExecQty),
|
|
976
|
+
fee=Decimal(data.cumExecFee or "0"),
|
|
977
|
+
fee_currency=getattr(data, "feeCurrency", None),
|
|
978
|
+
cum_cost=Decimal(data.cumExecValue or "0"),
|
|
979
|
+
reduce_only=data.reduceOnly,
|
|
980
|
+
position_side=BybitEnumParser.parse_position_side(position_idx),
|
|
981
|
+
)
|
|
982
|
+
|
|
983
|
+
async def query_order(self, oid: str, symbol: str) -> Order | None:
|
|
984
|
+
market = self._market.get(symbol)
|
|
985
|
+
if not market:
|
|
986
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
987
|
+
|
|
988
|
+
category = self._get_category(market)
|
|
989
|
+
params = {
|
|
990
|
+
"symbol": market.id,
|
|
991
|
+
"orderLinkId": oid,
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
try:
|
|
995
|
+
realtime = await self._api_client.get_v5_order_realtime(
|
|
996
|
+
category=category,
|
|
997
|
+
**params,
|
|
998
|
+
)
|
|
999
|
+
if realtime.retCode != 0:
|
|
1000
|
+
self._log.error(
|
|
1001
|
+
f"Error querying realtime order: {realtime.retMsg} params: {params}"
|
|
1002
|
+
)
|
|
1003
|
+
return None
|
|
1004
|
+
if realtime.result.list:
|
|
1005
|
+
return self._parse_rest_order(
|
|
1006
|
+
realtime.result.list[0],
|
|
1007
|
+
symbol,
|
|
1008
|
+
fallback_oid=oid,
|
|
1009
|
+
)
|
|
1010
|
+
|
|
1011
|
+
history = await self._api_client.get_v5_order_history(
|
|
1012
|
+
category=category,
|
|
1013
|
+
**params,
|
|
1014
|
+
)
|
|
1015
|
+
if history.retCode != 0:
|
|
1016
|
+
self._log.error(
|
|
1017
|
+
f"Error querying historical order: {history.retMsg} params: {params}"
|
|
1018
|
+
)
|
|
1019
|
+
return None
|
|
1020
|
+
if history.result.list:
|
|
1021
|
+
return self._parse_rest_order(
|
|
1022
|
+
history.result.list[0],
|
|
1023
|
+
symbol,
|
|
1024
|
+
fallback_oid=oid,
|
|
1025
|
+
)
|
|
1026
|
+
return None
|
|
1027
|
+
except Exception as e:
|
|
1028
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1029
|
+
self._log.error(f"Error querying order: {error_msg} params: {params}")
|
|
1030
|
+
return None
|
|
1031
|
+
|
|
1032
|
+
async def cancel_all_orders(self, symbol: str) -> bool:
|
|
1033
|
+
try:
|
|
1034
|
+
market = self._market.get(symbol)
|
|
1035
|
+
if not market:
|
|
1036
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1037
|
+
symbol = market.id
|
|
1038
|
+
|
|
1039
|
+
category = self._get_category(market)
|
|
1040
|
+
|
|
1041
|
+
params = {
|
|
1042
|
+
"category": category,
|
|
1043
|
+
"symbol": symbol,
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
await self._api_client.post_v5_order_cancel_all(**params)
|
|
1047
|
+
return True
|
|
1048
|
+
except Exception as e:
|
|
1049
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1050
|
+
self._log.error(
|
|
1051
|
+
f"Error canceling all orders: {error_msg} params: {str(params)}"
|
|
1052
|
+
)
|
|
1053
|
+
return False
|
|
1054
|
+
|
|
1055
|
+
async def modify_order(
|
|
1056
|
+
self,
|
|
1057
|
+
oid: str,
|
|
1058
|
+
symbol: str,
|
|
1059
|
+
side: OrderSide | None = None,
|
|
1060
|
+
price: Decimal | None = None,
|
|
1061
|
+
amount: Decimal | None = None,
|
|
1062
|
+
**kwargs,
|
|
1063
|
+
):
|
|
1064
|
+
# NOTE: side is not supported for modify order
|
|
1065
|
+
market = self._market.get(symbol)
|
|
1066
|
+
if not market:
|
|
1067
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1068
|
+
id = market.id
|
|
1069
|
+
|
|
1070
|
+
category = self._get_category(market)
|
|
1071
|
+
amend_kwargs: dict[str, str | None] = {
|
|
1072
|
+
"orderLinkId": oid,
|
|
1073
|
+
"price": str(price) if price else None,
|
|
1074
|
+
"qty": str(amount) if amount else None,
|
|
1075
|
+
**kwargs,
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
try:
|
|
1079
|
+
res = await self._api_client.post_v5_order_amend(
|
|
1080
|
+
category=category,
|
|
1081
|
+
symbol=id,
|
|
1082
|
+
**amend_kwargs,
|
|
1083
|
+
)
|
|
1084
|
+
order = Order(
|
|
1085
|
+
exchange=self._exchange_id,
|
|
1086
|
+
eid=res.result.orderId,
|
|
1087
|
+
oid=oid,
|
|
1088
|
+
timestamp=int(res.time),
|
|
1089
|
+
symbol=symbol,
|
|
1090
|
+
status=OrderStatus.PENDING,
|
|
1091
|
+
filled=Decimal(0),
|
|
1092
|
+
price=float(price) if price else None,
|
|
1093
|
+
remaining=amount,
|
|
1094
|
+
)
|
|
1095
|
+
return order
|
|
1096
|
+
except Exception as e:
|
|
1097
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1098
|
+
self._log.error(
|
|
1099
|
+
f"Error modifying order: {error_msg} params: {str(amend_kwargs)}"
|
|
1100
|
+
)
|
|
1101
|
+
order = Order(
|
|
1102
|
+
exchange=self._exchange_id,
|
|
1103
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1104
|
+
symbol=symbol,
|
|
1105
|
+
oid=oid,
|
|
1106
|
+
status=OrderStatus.FAILED,
|
|
1107
|
+
filled=Decimal(0),
|
|
1108
|
+
remaining=amount,
|
|
1109
|
+
price=float(price) if price else None,
|
|
1110
|
+
reason=error_msg,
|
|
1111
|
+
)
|
|
1112
|
+
return order
|
|
1113
|
+
|
|
1114
|
+
def _parse_order_update(self, raw: bytes):
|
|
1115
|
+
order_msg = self._ws_msg_order_update_decoder.decode(raw)
|
|
1116
|
+
self._log.debug(f"Order update: {str(order_msg)}")
|
|
1117
|
+
for data in order_msg.data:
|
|
1118
|
+
category = data.category
|
|
1119
|
+
if category.is_spot:
|
|
1120
|
+
market_id = data.symbol + "_spot"
|
|
1121
|
+
elif category.is_linear:
|
|
1122
|
+
market_id = data.symbol + "_linear"
|
|
1123
|
+
elif category.is_inverse:
|
|
1124
|
+
market_id = data.symbol + "_inverse"
|
|
1125
|
+
else:
|
|
1126
|
+
raise ValueError(f"Unsupported order category: {category}")
|
|
1127
|
+
symbol = self._market_id[market_id]
|
|
1128
|
+
time_in_force = BybitEnumParser.parse_time_in_force(data.timeInForce)
|
|
1129
|
+
|
|
1130
|
+
order = Order(
|
|
1131
|
+
exchange=self._exchange_id,
|
|
1132
|
+
symbol=symbol,
|
|
1133
|
+
status=BybitEnumParser.parse_order_status(data.orderStatus),
|
|
1134
|
+
eid=data.orderId,
|
|
1135
|
+
oid=data.orderLinkId,
|
|
1136
|
+
timestamp=int(data.updatedTime),
|
|
1137
|
+
type=BybitEnumParser.parse_order_type(data.orderType, time_in_force),
|
|
1138
|
+
side=BybitEnumParser.parse_order_side(data.side),
|
|
1139
|
+
time_in_force=time_in_force,
|
|
1140
|
+
price=float(data.price),
|
|
1141
|
+
average=float(data.avgPrice) if data.avgPrice else None,
|
|
1142
|
+
amount=Decimal(data.qty),
|
|
1143
|
+
filled=Decimal(data.cumExecQty),
|
|
1144
|
+
remaining=Decimal(data.qty) - Decimal(data.cumExecQty),
|
|
1145
|
+
fee=Decimal(data.cumExecFee),
|
|
1146
|
+
fee_currency=data.feeCurrency,
|
|
1147
|
+
cum_cost=Decimal(data.cumExecValue),
|
|
1148
|
+
reduce_only=data.reduceOnly,
|
|
1149
|
+
position_side=BybitEnumParser.parse_position_side(data.positionIdx),
|
|
1150
|
+
)
|
|
1151
|
+
|
|
1152
|
+
self.order_status_update(order)
|
|
1153
|
+
|
|
1154
|
+
def _parse_position_update(self, raw: bytes):
|
|
1155
|
+
position_msg = self._ws_msg_position_decoder.decode(raw)
|
|
1156
|
+
self._log.debug(f"Position update: {str(position_msg)}")
|
|
1157
|
+
|
|
1158
|
+
for data in position_msg.data:
|
|
1159
|
+
category = data.category
|
|
1160
|
+
if category.is_linear: # only linear/inverse/ position is supported
|
|
1161
|
+
market_id = data.symbol + "_linear"
|
|
1162
|
+
elif category.is_inverse:
|
|
1163
|
+
market_id = data.symbol + "_inverse"
|
|
1164
|
+
else:
|
|
1165
|
+
raise ValueError(f"Unsupported position category: {category}")
|
|
1166
|
+
symbol = self._market_id[market_id]
|
|
1167
|
+
|
|
1168
|
+
side = data.side.parse_to_position_side()
|
|
1169
|
+
if side == PositionSide.LONG:
|
|
1170
|
+
signed_amount = Decimal(data.size)
|
|
1171
|
+
elif side == PositionSide.SHORT:
|
|
1172
|
+
signed_amount = -Decimal(data.size)
|
|
1173
|
+
else:
|
|
1174
|
+
side = None
|
|
1175
|
+
signed_amount = Decimal(0)
|
|
1176
|
+
|
|
1177
|
+
position = Position(
|
|
1178
|
+
symbol=symbol,
|
|
1179
|
+
exchange=self._exchange_id,
|
|
1180
|
+
side=side,
|
|
1181
|
+
signed_amount=signed_amount,
|
|
1182
|
+
entry_price=float(data.entryPrice),
|
|
1183
|
+
unrealized_pnl=float(data.unrealisedPnl),
|
|
1184
|
+
realized_pnl=float(data.cumRealisedPnl),
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
self._cache._apply_position(position)
|
|
1188
|
+
|
|
1189
|
+
def _parse_wallet_update(self, raw: bytes):
|
|
1190
|
+
wallet_msg = self._ws_msg_wallet_decoder.decode(raw)
|
|
1191
|
+
self._log.debug(f"Wallet update: {str(wallet_msg)}")
|
|
1192
|
+
|
|
1193
|
+
for data in wallet_msg.data:
|
|
1194
|
+
balances = data.parse_to_balances()
|
|
1195
|
+
self._cache._apply_balance(self._account_type, balances)
|