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,1619 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
from typing import Any, Dict, List, cast
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from decimal import ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
|
|
6
|
+
from walrasquant.error import PositionModeError
|
|
7
|
+
from walrasquant.config import OrderQueryConfig
|
|
8
|
+
from walrasquant.exchange.bitget.error import BitgetRateLimitError, BitgetError
|
|
9
|
+
from walrasquant.core.nautilius_core import LiveClock, MessageBus
|
|
10
|
+
from walrasquant.core.cache import AsyncCache
|
|
11
|
+
from walrasquant.base import OrderManagementSystem
|
|
12
|
+
from walrasquant.core.entity import TaskManager
|
|
13
|
+
from walrasquant.core.registry import OrderRegistry
|
|
14
|
+
from walrasquant.schema import (
|
|
15
|
+
Order,
|
|
16
|
+
Position,
|
|
17
|
+
BatchOrderSubmit,
|
|
18
|
+
BaseMarket,
|
|
19
|
+
)
|
|
20
|
+
from walrasquant.constants import (
|
|
21
|
+
ExchangeType,
|
|
22
|
+
OrderSide,
|
|
23
|
+
OrderStatus,
|
|
24
|
+
OrderType,
|
|
25
|
+
TimeInForce,
|
|
26
|
+
TriggerType,
|
|
27
|
+
)
|
|
28
|
+
from walrasquant.exchange.bitget.schema import (
|
|
29
|
+
BitgetMarket,
|
|
30
|
+
BitgetWsGeneralMsg,
|
|
31
|
+
BitgetWsUtaGeneralMsg,
|
|
32
|
+
BitgetWsArgMsg,
|
|
33
|
+
BitgetOrderWsMsg,
|
|
34
|
+
BitgetPositionWsMsg,
|
|
35
|
+
BitgetUtaOrderWsMsg,
|
|
36
|
+
BitgetUtaPositionWsMsg,
|
|
37
|
+
BitgetSpotAccountWsMsg,
|
|
38
|
+
BitgetFuturesAccountWsMsg,
|
|
39
|
+
BitgetUtaAccountWsMsg,
|
|
40
|
+
BitgetWsApiGeneralMsg,
|
|
41
|
+
BitgetWsApiArgMsg,
|
|
42
|
+
BitgetWsApiUtaGeneralMsg,
|
|
43
|
+
)
|
|
44
|
+
from walrasquant.exchange.bitget.rest_api import BitgetApiClient
|
|
45
|
+
from walrasquant.exchange.bitget.websockets import BitgetWSClient, BitgetWSApiClient
|
|
46
|
+
from walrasquant.exchange.bitget.constants import (
|
|
47
|
+
BitgetAccountType,
|
|
48
|
+
BitgetInstType,
|
|
49
|
+
BitgetUtaInstType,
|
|
50
|
+
BitgetEnumParser,
|
|
51
|
+
BitgetOrderSide,
|
|
52
|
+
BitgetOrderStatus,
|
|
53
|
+
BitgetOrderType,
|
|
54
|
+
BitgetPositionSide,
|
|
55
|
+
BitgetTimeInForce,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class BitgetOrderManagementSystem(OrderManagementSystem):
|
|
60
|
+
_inst_type_map = {
|
|
61
|
+
BitgetInstType.SPOT: "spot",
|
|
62
|
+
BitgetInstType.USDC_FUTURES: "linear",
|
|
63
|
+
BitgetInstType.USDT_FUTURES: "linear",
|
|
64
|
+
BitgetInstType.COIN_FUTURES: "inverse",
|
|
65
|
+
}
|
|
66
|
+
_uta_inst_type_map = {
|
|
67
|
+
BitgetUtaInstType.SPOT: "spot",
|
|
68
|
+
BitgetUtaInstType.USDC_FUTURES: "linear",
|
|
69
|
+
BitgetUtaInstType.USDT_FUTURES: "linear",
|
|
70
|
+
BitgetUtaInstType.COIN_FUTURES: "inverse",
|
|
71
|
+
}
|
|
72
|
+
_ws_client: BitgetWSClient
|
|
73
|
+
_account_type: BitgetAccountType
|
|
74
|
+
_market: Dict[str, BitgetMarket]
|
|
75
|
+
_market_id: Dict[str, str]
|
|
76
|
+
_api_client: BitgetApiClient
|
|
77
|
+
|
|
78
|
+
def __init__(
|
|
79
|
+
self,
|
|
80
|
+
account_type: BitgetAccountType,
|
|
81
|
+
api_key: str,
|
|
82
|
+
secret: str,
|
|
83
|
+
passphrase: str,
|
|
84
|
+
market: Dict[str, BitgetMarket],
|
|
85
|
+
market_id: Dict[str, str],
|
|
86
|
+
registry: OrderRegistry,
|
|
87
|
+
cache: AsyncCache,
|
|
88
|
+
api_client: BitgetApiClient,
|
|
89
|
+
exchange_id: ExchangeType,
|
|
90
|
+
clock: LiveClock,
|
|
91
|
+
msgbus: MessageBus,
|
|
92
|
+
task_manager: TaskManager,
|
|
93
|
+
max_slippage: float,
|
|
94
|
+
enable_rate_limit: bool,
|
|
95
|
+
order_query_config: OrderQueryConfig,
|
|
96
|
+
max_subscriptions_per_client: int | None = None,
|
|
97
|
+
max_clients: int | None = None,
|
|
98
|
+
):
|
|
99
|
+
super().__init__(
|
|
100
|
+
account_type=account_type,
|
|
101
|
+
market=cast(dict[str, BaseMarket], market),
|
|
102
|
+
market_id=market_id,
|
|
103
|
+
registry=registry,
|
|
104
|
+
cache=cache,
|
|
105
|
+
api_client=api_client,
|
|
106
|
+
ws_client=BitgetWSClient(
|
|
107
|
+
account_type=account_type,
|
|
108
|
+
handler=self._ws_uta_msg_handler
|
|
109
|
+
if account_type.is_uta
|
|
110
|
+
else self._ws_msg_handler,
|
|
111
|
+
clock=clock,
|
|
112
|
+
task_manager=task_manager,
|
|
113
|
+
api_key=api_key,
|
|
114
|
+
secret=secret,
|
|
115
|
+
passphrase=passphrase,
|
|
116
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
117
|
+
max_clients=max_clients,
|
|
118
|
+
),
|
|
119
|
+
exchange_id=exchange_id,
|
|
120
|
+
clock=clock,
|
|
121
|
+
msgbus=msgbus,
|
|
122
|
+
task_manager=task_manager,
|
|
123
|
+
order_query_config=order_query_config,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
self._ws_api_client = BitgetWSApiClient(
|
|
127
|
+
account_type=account_type,
|
|
128
|
+
api_key=api_key,
|
|
129
|
+
secret=secret,
|
|
130
|
+
passphrase=passphrase,
|
|
131
|
+
handler=self._ws_uta_api_msg_handler
|
|
132
|
+
if account_type.is_uta
|
|
133
|
+
else self._ws_api_msg_handler,
|
|
134
|
+
task_manager=task_manager,
|
|
135
|
+
clock=clock,
|
|
136
|
+
enable_rate_limit=enable_rate_limit,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
self._max_slippage = max_slippage
|
|
140
|
+
self._ws_msg_general_decoder = msgspec.json.Decoder(BitgetWsGeneralMsg)
|
|
141
|
+
self._ws_msg_uta_general_decoder = msgspec.json.Decoder(BitgetWsUtaGeneralMsg)
|
|
142
|
+
self._ws_msg_orders_decoder = msgspec.json.Decoder(BitgetOrderWsMsg)
|
|
143
|
+
self._ws_msg_uta_orders_decoder = msgspec.json.Decoder(BitgetUtaOrderWsMsg)
|
|
144
|
+
self._ws_msg_positions_decoder = msgspec.json.Decoder(BitgetPositionWsMsg)
|
|
145
|
+
self._ws_msg_uta_positions_decoder = msgspec.json.Decoder(
|
|
146
|
+
BitgetUtaPositionWsMsg
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
self._ws_msg_spot_account_decoder = msgspec.json.Decoder(BitgetSpotAccountWsMsg)
|
|
150
|
+
self._ws_msg_futures_account_decoder = msgspec.json.Decoder(
|
|
151
|
+
BitgetFuturesAccountWsMsg
|
|
152
|
+
)
|
|
153
|
+
self._ws_msg_uta_account_decoder = msgspec.json.Decoder(BitgetUtaAccountWsMsg)
|
|
154
|
+
|
|
155
|
+
self._ws_api_general_decoder = msgspec.json.Decoder(BitgetWsApiGeneralMsg)
|
|
156
|
+
self._ws_api_uta_general_decoder = msgspec.json.Decoder(
|
|
157
|
+
BitgetWsApiUtaGeneralMsg
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
def _supports_query_market(self, market: BitgetMarket) -> bool:
|
|
161
|
+
if self._account_type.is_uta:
|
|
162
|
+
return market.spot or market.swap
|
|
163
|
+
if self._account_type.is_spot:
|
|
164
|
+
return market.spot
|
|
165
|
+
if self._account_type.is_future:
|
|
166
|
+
return market.swap
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
def _ws_uta_api_msg_handler(self, raw: bytes):
|
|
170
|
+
# if raw == b"pong":
|
|
171
|
+
# self._ws_api_client._transport.notify_user_specific_pong_received()
|
|
172
|
+
# self._log.debug(f"Pong received: `{raw.decode()}`")
|
|
173
|
+
# return
|
|
174
|
+
try:
|
|
175
|
+
ws_msg = self._ws_api_uta_general_decoder.decode(raw)
|
|
176
|
+
|
|
177
|
+
if ws_msg.is_id_msg:
|
|
178
|
+
self._handle_id_messages(ws_msg)
|
|
179
|
+
elif ws_msg.is_error_msg:
|
|
180
|
+
self._log.error(f"login Error: {ws_msg.error_msg}")
|
|
181
|
+
|
|
182
|
+
except msgspec.DecodeError as e:
|
|
183
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
184
|
+
|
|
185
|
+
def _ws_api_msg_handler(self, raw: bytes):
|
|
186
|
+
# if raw == b"pong":
|
|
187
|
+
# self._ws_api_client._transport.notify_user_specific_pong_received()
|
|
188
|
+
# self._log.debug(f"Pong received: `{raw.decode()}`")
|
|
189
|
+
# return
|
|
190
|
+
try:
|
|
191
|
+
ws_msg = self._ws_api_general_decoder.decode(raw)
|
|
192
|
+
|
|
193
|
+
if ws_msg.is_arg_msg:
|
|
194
|
+
self._handle_arg_messages(ws_msg)
|
|
195
|
+
elif ws_msg.is_error_msg:
|
|
196
|
+
self._log.error(f"login Error: {ws_msg.error_msg}")
|
|
197
|
+
|
|
198
|
+
except msgspec.DecodeError as e:
|
|
199
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
200
|
+
|
|
201
|
+
def _handle_id_messages(self, ws_msg: BitgetWsApiUtaGeneralMsg):
|
|
202
|
+
"""Handle argument messages for place and cancel orders"""
|
|
203
|
+
if ws_msg.id and ws_msg.id.startswith("n"):
|
|
204
|
+
self._handle_uta_place_order_response(ws_msg)
|
|
205
|
+
else:
|
|
206
|
+
self._handle_uta_cancel_order_response(ws_msg)
|
|
207
|
+
|
|
208
|
+
def _handle_arg_messages(self, ws_msg: BitgetWsApiGeneralMsg):
|
|
209
|
+
"""Handle argument messages for place and cancel orders"""
|
|
210
|
+
for arg_msg in ws_msg.arg or []:
|
|
211
|
+
if arg_msg.is_place_order:
|
|
212
|
+
self._handle_place_order_response(ws_msg, arg_msg)
|
|
213
|
+
elif arg_msg.is_cancel_order:
|
|
214
|
+
self._handle_cancel_order_response(ws_msg, arg_msg)
|
|
215
|
+
|
|
216
|
+
def _handle_uta_place_order_response(self, ws_msg: BitgetWsApiUtaGeneralMsg):
|
|
217
|
+
oid = ws_msg.oid
|
|
218
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
219
|
+
if not tmp_order:
|
|
220
|
+
return
|
|
221
|
+
ts = self._clock.timestamp_ms()
|
|
222
|
+
|
|
223
|
+
if ws_msg.is_success:
|
|
224
|
+
for arg_msg in ws_msg.args or []:
|
|
225
|
+
ordId = arg_msg.orderId
|
|
226
|
+
self._log.debug(
|
|
227
|
+
f"[{tmp_order.symbol}] placing order success: oid: {oid} id: {ordId}"
|
|
228
|
+
)
|
|
229
|
+
order = self._create_order_from_tmp(
|
|
230
|
+
tmp_order, oid, ordId, OrderStatus.PENDING, ts
|
|
231
|
+
)
|
|
232
|
+
self.order_status_update(order) # INITIALIZED -> PENDING
|
|
233
|
+
else:
|
|
234
|
+
self._log.error(
|
|
235
|
+
f"[{tmp_order.symbol}] new order failed: oid: {oid} {ws_msg.error_msg}"
|
|
236
|
+
)
|
|
237
|
+
order = self._create_order_from_tmp(
|
|
238
|
+
tmp_order,
|
|
239
|
+
oid,
|
|
240
|
+
None,
|
|
241
|
+
OrderStatus.FAILED,
|
|
242
|
+
ts,
|
|
243
|
+
reason=ws_msg.error_msg,
|
|
244
|
+
)
|
|
245
|
+
self.order_status_update(order)
|
|
246
|
+
|
|
247
|
+
def _handle_uta_cancel_order_response(self, ws_msg: BitgetWsApiUtaGeneralMsg):
|
|
248
|
+
"""Handle cancel order response"""
|
|
249
|
+
oid = ws_msg.oid
|
|
250
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
251
|
+
if not tmp_order:
|
|
252
|
+
return
|
|
253
|
+
ts = self._clock.timestamp_ms()
|
|
254
|
+
|
|
255
|
+
if ws_msg.is_success:
|
|
256
|
+
for arg_msg in ws_msg.args or []:
|
|
257
|
+
ordId = arg_msg.orderId
|
|
258
|
+
self._log.debug(
|
|
259
|
+
f"[{tmp_order.symbol}] canceling order success: oid: {oid} id: {ordId}"
|
|
260
|
+
)
|
|
261
|
+
order = self._create_order_from_tmp(
|
|
262
|
+
tmp_order, oid, ordId, OrderStatus.CANCELING, ts
|
|
263
|
+
)
|
|
264
|
+
self.order_status_update(order) # SOME STATUS -> CANCELING
|
|
265
|
+
else:
|
|
266
|
+
self._log.error(
|
|
267
|
+
f"[{tmp_order.symbol}] canceling order failed: oid: {oid} {ws_msg.error_msg}"
|
|
268
|
+
)
|
|
269
|
+
order = self._create_order_from_tmp(
|
|
270
|
+
tmp_order,
|
|
271
|
+
oid,
|
|
272
|
+
None,
|
|
273
|
+
OrderStatus.CANCEL_FAILED,
|
|
274
|
+
ts,
|
|
275
|
+
reason=ws_msg.error_msg,
|
|
276
|
+
)
|
|
277
|
+
self.order_status_update(order)
|
|
278
|
+
|
|
279
|
+
def _handle_place_order_response(
|
|
280
|
+
self, ws_msg: BitgetWsApiGeneralMsg, arg_msg: BitgetWsApiArgMsg
|
|
281
|
+
):
|
|
282
|
+
"""Handle place order response"""
|
|
283
|
+
oid = arg_msg.id
|
|
284
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
285
|
+
if not tmp_order:
|
|
286
|
+
return
|
|
287
|
+
ts = self._clock.timestamp_ms()
|
|
288
|
+
|
|
289
|
+
if ws_msg.is_success:
|
|
290
|
+
ordId = arg_msg.params.orderId
|
|
291
|
+
self._log.debug(
|
|
292
|
+
f"[{tmp_order.symbol}] placing order success: oid: {oid} id: {ordId}"
|
|
293
|
+
)
|
|
294
|
+
order = self._create_order_from_tmp(
|
|
295
|
+
tmp_order, oid, ordId, OrderStatus.PENDING, ts
|
|
296
|
+
)
|
|
297
|
+
self.order_status_update(order) # INITIALIZED -> PENDING
|
|
298
|
+
else:
|
|
299
|
+
self._log.error(
|
|
300
|
+
f"[{tmp_order.symbol}] new order failed: oid: {oid} {ws_msg.error_msg}"
|
|
301
|
+
)
|
|
302
|
+
order = self._create_order_from_tmp(
|
|
303
|
+
tmp_order,
|
|
304
|
+
oid,
|
|
305
|
+
None,
|
|
306
|
+
OrderStatus.FAILED,
|
|
307
|
+
ts,
|
|
308
|
+
reason=ws_msg.error_msg,
|
|
309
|
+
)
|
|
310
|
+
self.order_status_update(order) # INITIALIZED -> FAILED
|
|
311
|
+
|
|
312
|
+
def _handle_cancel_order_response(
|
|
313
|
+
self, ws_msg: BitgetWsApiGeneralMsg, arg_msg: BitgetWsApiArgMsg
|
|
314
|
+
):
|
|
315
|
+
"""Handle cancel order response"""
|
|
316
|
+
oid = arg_msg.id
|
|
317
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
318
|
+
if not tmp_order:
|
|
319
|
+
return
|
|
320
|
+
ts = self._clock.timestamp_ms()
|
|
321
|
+
|
|
322
|
+
if ws_msg.is_success:
|
|
323
|
+
ordId = arg_msg.params.orderId
|
|
324
|
+
self._log.debug(
|
|
325
|
+
f"[{tmp_order.symbol}] canceling order success: oid: {oid} id: {ordId}"
|
|
326
|
+
)
|
|
327
|
+
order = self._create_order_from_tmp(
|
|
328
|
+
tmp_order, oid, ordId, OrderStatus.CANCELING, ts
|
|
329
|
+
)
|
|
330
|
+
self.order_status_update(order)
|
|
331
|
+
else:
|
|
332
|
+
self._log.error(
|
|
333
|
+
f"[{tmp_order.symbol}] canceling order failed: oid: {oid} {ws_msg.error_msg}"
|
|
334
|
+
)
|
|
335
|
+
order = self._create_order_from_tmp(
|
|
336
|
+
tmp_order,
|
|
337
|
+
oid,
|
|
338
|
+
None,
|
|
339
|
+
OrderStatus.CANCEL_FAILED,
|
|
340
|
+
ts,
|
|
341
|
+
reason=ws_msg.error_msg,
|
|
342
|
+
)
|
|
343
|
+
self.order_status_update(order)
|
|
344
|
+
|
|
345
|
+
def _create_order_from_tmp(
|
|
346
|
+
self,
|
|
347
|
+
tmp_order: Order,
|
|
348
|
+
oid: str,
|
|
349
|
+
order_id: str | None,
|
|
350
|
+
status: OrderStatus,
|
|
351
|
+
timestamp: int,
|
|
352
|
+
reason: str | None = None,
|
|
353
|
+
) -> Order:
|
|
354
|
+
"""Create Order object from temporary order data"""
|
|
355
|
+
return Order(
|
|
356
|
+
exchange=self._exchange_id,
|
|
357
|
+
symbol=tmp_order.symbol,
|
|
358
|
+
oid=oid,
|
|
359
|
+
eid=order_id,
|
|
360
|
+
side=tmp_order.side,
|
|
361
|
+
status=status,
|
|
362
|
+
amount=tmp_order.amount,
|
|
363
|
+
timestamp=timestamp,
|
|
364
|
+
type=tmp_order.type,
|
|
365
|
+
price=tmp_order.price,
|
|
366
|
+
time_in_force=tmp_order.time_in_force,
|
|
367
|
+
reduce_only=tmp_order.reduce_only,
|
|
368
|
+
reason=reason,
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
def _inst_type_suffix(self, inst_type: BitgetInstType):
|
|
372
|
+
return self._inst_type_map[inst_type]
|
|
373
|
+
|
|
374
|
+
def _uta_inst_type_suffix(self, category: BitgetUtaInstType) -> str:
|
|
375
|
+
"""Convert UTA category to internal suffix"""
|
|
376
|
+
return self._uta_inst_type_map[category]
|
|
377
|
+
|
|
378
|
+
def _get_inst_type(self, market: BitgetMarket):
|
|
379
|
+
if market.spot:
|
|
380
|
+
return "SPOT"
|
|
381
|
+
elif market.linear:
|
|
382
|
+
return "USDT-FUTURES" if market.quote == "USDT" else "USDC-FUTURES"
|
|
383
|
+
elif market.inverse:
|
|
384
|
+
return "COIN-FUTURES"
|
|
385
|
+
|
|
386
|
+
async def _init_account_balance(self):
|
|
387
|
+
"""Initialize account balance"""
|
|
388
|
+
pass
|
|
389
|
+
|
|
390
|
+
async def _init_position(self):
|
|
391
|
+
"""Initialize position"""
|
|
392
|
+
# Bitget has different product types for different instrument types
|
|
393
|
+
|
|
394
|
+
if self._account_type.is_uta:
|
|
395
|
+
# For UTA accounts, use the v3 position API
|
|
396
|
+
categories = ["USDT-FUTURES", "USDC-FUTURES", "COIN-FUTURES"]
|
|
397
|
+
|
|
398
|
+
for category in categories:
|
|
399
|
+
response = await self._api_client.get_api_v3_position_current_position(
|
|
400
|
+
category=category
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if not response.data.list:
|
|
404
|
+
continue
|
|
405
|
+
|
|
406
|
+
for pos_data in response.data.list:
|
|
407
|
+
# Skip positions with zero total
|
|
408
|
+
if float(pos_data.total) == 0:
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
# Check position mode - only support one-way mode for UTA
|
|
412
|
+
if pos_data.holdMode != "one_way_mode":
|
|
413
|
+
raise PositionModeError(
|
|
414
|
+
f"Only one-way mode is supported for UTA accounts. Current mode: {pos_data.holdMode}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Determine suffix based on category
|
|
418
|
+
if category == "USDT-FUTURES":
|
|
419
|
+
inst_type_suffix = "linear"
|
|
420
|
+
elif category == "USDC-FUTURES":
|
|
421
|
+
inst_type_suffix = "linear"
|
|
422
|
+
elif category == "COIN-FUTURES":
|
|
423
|
+
inst_type_suffix = "inverse"
|
|
424
|
+
|
|
425
|
+
# Map symbol from Bitget format to our internal format
|
|
426
|
+
symbol = self._market_id.get(
|
|
427
|
+
f"{pos_data.symbol}_{inst_type_suffix}"
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if not symbol:
|
|
431
|
+
self._log.warning(
|
|
432
|
+
f"Symbol {pos_data.symbol} not found in market mapping"
|
|
433
|
+
)
|
|
434
|
+
continue
|
|
435
|
+
|
|
436
|
+
# Convert position side string to BitgetPositionSide enum
|
|
437
|
+
hold_side = BitgetPositionSide(pos_data.posSide)
|
|
438
|
+
|
|
439
|
+
# Convert to signed amount based on position side
|
|
440
|
+
signed_amount = Decimal(pos_data.total)
|
|
441
|
+
if hold_side == BitgetPositionSide.SHORT:
|
|
442
|
+
signed_amount = -signed_amount
|
|
443
|
+
|
|
444
|
+
# Parse position side
|
|
445
|
+
position_side = hold_side.parse_to_position_side()
|
|
446
|
+
|
|
447
|
+
# Create Position object
|
|
448
|
+
position = Position(
|
|
449
|
+
symbol=symbol,
|
|
450
|
+
exchange=self._exchange_id,
|
|
451
|
+
signed_amount=signed_amount,
|
|
452
|
+
entry_price=float(pos_data.avgPrice),
|
|
453
|
+
side=position_side,
|
|
454
|
+
unrealized_pnl=float(pos_data.unrealisedPnl),
|
|
455
|
+
realized_pnl=float(pos_data.curRealisedPnl),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
# Apply position to cache
|
|
459
|
+
self._cache._apply_position(position)
|
|
460
|
+
self._log.debug(f"Initialized UTA position: {str(position)}")
|
|
461
|
+
|
|
462
|
+
else:
|
|
463
|
+
product_types = ["USDT-FUTURES", "USDC-FUTURES", "COIN-FUTURES"]
|
|
464
|
+
|
|
465
|
+
for product_type in product_types:
|
|
466
|
+
# Get all positions for this product type
|
|
467
|
+
response = await self._api_client.get_api_v2_mix_position_all_position(
|
|
468
|
+
productType=product_type
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
for pos_data in response.data:
|
|
472
|
+
# Check position mode - only support one-way mode
|
|
473
|
+
if pos_data.posMode != "one_way_mode":
|
|
474
|
+
raise PositionModeError(
|
|
475
|
+
f"Only one-way position mode is supported. Current mode: {pos_data.posMode}"
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Skip positions with zero amount
|
|
479
|
+
if float(pos_data.total) == 0:
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
# Map symbol from Bitget format to our internal format
|
|
483
|
+
inst_type = BitgetInstType(product_type)
|
|
484
|
+
inst_type_suffix = self._inst_type_suffix(inst_type)
|
|
485
|
+
symbol = self._market_id.get(
|
|
486
|
+
f"{pos_data.symbol}_{inst_type_suffix}"
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
if not symbol:
|
|
490
|
+
self._log.warning(
|
|
491
|
+
f"Symbol {pos_data.symbol} not found in market mapping"
|
|
492
|
+
)
|
|
493
|
+
continue
|
|
494
|
+
|
|
495
|
+
# Convert holdSide string to BitgetPositionSide enum
|
|
496
|
+
hold_side = BitgetPositionSide(pos_data.holdSide)
|
|
497
|
+
|
|
498
|
+
# Convert to signed amount based on position side
|
|
499
|
+
signed_amount = Decimal(pos_data.total)
|
|
500
|
+
if hold_side == BitgetPositionSide.SHORT:
|
|
501
|
+
signed_amount = -signed_amount
|
|
502
|
+
|
|
503
|
+
# Parse position side
|
|
504
|
+
position_side = hold_side.parse_to_position_side()
|
|
505
|
+
|
|
506
|
+
# Create Position object
|
|
507
|
+
position = Position(
|
|
508
|
+
symbol=symbol,
|
|
509
|
+
exchange=self._exchange_id,
|
|
510
|
+
signed_amount=signed_amount,
|
|
511
|
+
entry_price=float(pos_data.openPriceAvg),
|
|
512
|
+
side=position_side,
|
|
513
|
+
unrealized_pnl=float(pos_data.unrealizedPL),
|
|
514
|
+
realized_pnl=float(pos_data.achievedProfits or 0.0),
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Apply position to cache
|
|
518
|
+
self._cache._apply_position(position)
|
|
519
|
+
self._log.debug(f"Initialized position: {str(position)}")
|
|
520
|
+
|
|
521
|
+
async def _position_mode_check(self):
|
|
522
|
+
"""Check the position mode"""
|
|
523
|
+
# Position mode check is performed in _init_position for each position
|
|
524
|
+
# Only one-way mode is supported
|
|
525
|
+
pass
|
|
526
|
+
|
|
527
|
+
async def create_tp_sl_order(
|
|
528
|
+
self,
|
|
529
|
+
oid: str,
|
|
530
|
+
symbol: str,
|
|
531
|
+
side: OrderSide,
|
|
532
|
+
type: OrderType,
|
|
533
|
+
amount: Decimal,
|
|
534
|
+
price: Decimal | None = None,
|
|
535
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
536
|
+
tp_order_type: OrderType | None = None,
|
|
537
|
+
tp_trigger_price: Decimal | None = None,
|
|
538
|
+
tp_price: Decimal | None = None,
|
|
539
|
+
tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
|
|
540
|
+
sl_order_type: OrderType | None = None,
|
|
541
|
+
sl_trigger_price: Decimal | None = None,
|
|
542
|
+
sl_price: Decimal | None = None,
|
|
543
|
+
sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
|
|
544
|
+
**kwargs,
|
|
545
|
+
) -> Order:
|
|
546
|
+
"""Create a take profit and stop loss order"""
|
|
547
|
+
raise NotImplementedError
|
|
548
|
+
|
|
549
|
+
def _price_to_precision(
|
|
550
|
+
self,
|
|
551
|
+
symbol: str,
|
|
552
|
+
price: float,
|
|
553
|
+
mode: Literal["round", "ceil", "floor"] = "round",
|
|
554
|
+
) -> Decimal:
|
|
555
|
+
market = self._market[symbol]
|
|
556
|
+
if market.spot:
|
|
557
|
+
return super()._price_to_precision(symbol, price, mode)
|
|
558
|
+
else:
|
|
559
|
+
price: Decimal = Decimal(str(price))
|
|
560
|
+
price_multiplier = Decimal(market.info.priceEndStep or "1")
|
|
561
|
+
multiplier_count = price / price_multiplier
|
|
562
|
+
|
|
563
|
+
if mode == "round":
|
|
564
|
+
price = (
|
|
565
|
+
multiplier_count.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
|
566
|
+
) * price_multiplier
|
|
567
|
+
elif mode == "ceil":
|
|
568
|
+
price = (
|
|
569
|
+
multiplier_count.quantize(Decimal("1"), rounding=ROUND_CEILING)
|
|
570
|
+
) * price_multiplier
|
|
571
|
+
elif mode == "floor":
|
|
572
|
+
price = (
|
|
573
|
+
multiplier_count.quantize(Decimal("1"), rounding=ROUND_FLOOR)
|
|
574
|
+
) * price_multiplier
|
|
575
|
+
return price
|
|
576
|
+
|
|
577
|
+
async def create_order_ws(
|
|
578
|
+
self,
|
|
579
|
+
oid: str,
|
|
580
|
+
symbol: str,
|
|
581
|
+
side: OrderSide,
|
|
582
|
+
type: OrderType,
|
|
583
|
+
amount: Decimal,
|
|
584
|
+
price: Decimal | None,
|
|
585
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
586
|
+
reduce_only: bool = False,
|
|
587
|
+
**kwargs,
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Create an order"""
|
|
590
|
+
self._registry.register_tmp_order(
|
|
591
|
+
order=Order(
|
|
592
|
+
oid=oid,
|
|
593
|
+
exchange=self._exchange_id,
|
|
594
|
+
symbol=symbol,
|
|
595
|
+
status=OrderStatus.INITIALIZED,
|
|
596
|
+
amount=amount,
|
|
597
|
+
type=type,
|
|
598
|
+
side=side,
|
|
599
|
+
price=float(price) if price else None,
|
|
600
|
+
time_in_force=time_in_force,
|
|
601
|
+
timestamp=self._clock.timestamp_ms(),
|
|
602
|
+
reduce_only=reduce_only,
|
|
603
|
+
)
|
|
604
|
+
)
|
|
605
|
+
market = self._market.get(symbol)
|
|
606
|
+
if not market:
|
|
607
|
+
raise ValueError(f"Symbol {symbol} not found in market data.")
|
|
608
|
+
|
|
609
|
+
if self._account_type.is_uta:
|
|
610
|
+
category: str = kwargs.pop("category", self._get_inst_type(market))
|
|
611
|
+
params = {
|
|
612
|
+
"category": category.lower(),
|
|
613
|
+
"symbol": market.id,
|
|
614
|
+
"side": BitgetEnumParser.to_bitget_order_side(side).value,
|
|
615
|
+
"qty": str(amount),
|
|
616
|
+
"clientOid": oid,
|
|
617
|
+
}
|
|
618
|
+
if type.is_limit:
|
|
619
|
+
if price is None or time_in_force is None:
|
|
620
|
+
raise ValueError(
|
|
621
|
+
"price and time_in_force are required for limit orders"
|
|
622
|
+
)
|
|
623
|
+
params["price"] = str(price)
|
|
624
|
+
params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
625
|
+
time_in_force
|
|
626
|
+
).value
|
|
627
|
+
params["orderType"] = "limit"
|
|
628
|
+
elif type.is_post_only:
|
|
629
|
+
if price is None:
|
|
630
|
+
raise ValueError("price is required for post-only orders")
|
|
631
|
+
params["price"] = str(price)
|
|
632
|
+
params["timeInForce"] = "post_only"
|
|
633
|
+
params["orderType"] = "limit"
|
|
634
|
+
elif type.is_market:
|
|
635
|
+
bookl1 = self._cache.bookl1(symbol)
|
|
636
|
+
if not bookl1:
|
|
637
|
+
raise ValueError(
|
|
638
|
+
"Please Subscribe to bookl1 first, since market order requires bookl1 data"
|
|
639
|
+
)
|
|
640
|
+
if side.is_buy:
|
|
641
|
+
price = self._price_to_precision(
|
|
642
|
+
symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
|
|
643
|
+
)
|
|
644
|
+
else:
|
|
645
|
+
price = self._price_to_precision(
|
|
646
|
+
symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
|
|
647
|
+
)
|
|
648
|
+
params["price"] = str(price)
|
|
649
|
+
params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
650
|
+
TimeInForce.IOC
|
|
651
|
+
).value
|
|
652
|
+
params["orderType"] = "limit"
|
|
653
|
+
|
|
654
|
+
if reduce_only:
|
|
655
|
+
params["reduceOnly"] = "yes"
|
|
656
|
+
|
|
657
|
+
params.update(kwargs)
|
|
658
|
+
|
|
659
|
+
try:
|
|
660
|
+
await self._ws_api_client.uta_place_order(id=oid, **params)
|
|
661
|
+
except BitgetRateLimitError as e:
|
|
662
|
+
order = self._rate_limit_failed_order(
|
|
663
|
+
oid=oid,
|
|
664
|
+
symbol=symbol,
|
|
665
|
+
side=side,
|
|
666
|
+
type=type,
|
|
667
|
+
amount=amount,
|
|
668
|
+
price=price,
|
|
669
|
+
time_in_force=time_in_force,
|
|
670
|
+
reduce_only=reduce_only,
|
|
671
|
+
exc=e,
|
|
672
|
+
)
|
|
673
|
+
self.order_status_update(order)
|
|
674
|
+
|
|
675
|
+
else:
|
|
676
|
+
params = {
|
|
677
|
+
"instId": market.id,
|
|
678
|
+
"side": BitgetEnumParser.to_bitget_order_side(side).value,
|
|
679
|
+
"size": str(amount),
|
|
680
|
+
"clientOid": oid,
|
|
681
|
+
}
|
|
682
|
+
if type.is_limit:
|
|
683
|
+
if price is None or time_in_force is None:
|
|
684
|
+
raise ValueError(
|
|
685
|
+
"price and time_in_force are required for limit orders"
|
|
686
|
+
)
|
|
687
|
+
params["price"] = str(price)
|
|
688
|
+
params["force"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
689
|
+
time_in_force
|
|
690
|
+
).value
|
|
691
|
+
params["orderType"] = "limit"
|
|
692
|
+
elif type.is_post_only:
|
|
693
|
+
if price is None:
|
|
694
|
+
raise ValueError("price is required for post-only orders")
|
|
695
|
+
params["price"] = str(price)
|
|
696
|
+
params["force"] = "post_only"
|
|
697
|
+
params["orderType"] = "limit"
|
|
698
|
+
elif type.is_market:
|
|
699
|
+
bookl1 = self._cache.bookl1(symbol)
|
|
700
|
+
if not bookl1:
|
|
701
|
+
raise ValueError(
|
|
702
|
+
"Please Subscribe to bookl1 first, since market order requires bookl1 data"
|
|
703
|
+
)
|
|
704
|
+
if side.is_buy:
|
|
705
|
+
price = self._price_to_precision(
|
|
706
|
+
symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
|
|
707
|
+
)
|
|
708
|
+
else:
|
|
709
|
+
price = self._price_to_precision(
|
|
710
|
+
symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
|
|
711
|
+
)
|
|
712
|
+
params["price"] = str(price)
|
|
713
|
+
params["force"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
714
|
+
TimeInForce.IOC
|
|
715
|
+
).value
|
|
716
|
+
params["orderType"] = "limit"
|
|
717
|
+
|
|
718
|
+
params.update(kwargs)
|
|
719
|
+
|
|
720
|
+
if market.swap:
|
|
721
|
+
params["marginCoin"] = market.quote
|
|
722
|
+
params["marginMode"] = kwargs.get("marginMode", "crossed")
|
|
723
|
+
params["instType"] = self._get_inst_type(market)
|
|
724
|
+
if reduce_only:
|
|
725
|
+
params["reduceOnly"] = "YES"
|
|
726
|
+
try:
|
|
727
|
+
await self._ws_api_client.future_place_order(id=oid, **params)
|
|
728
|
+
except BitgetRateLimitError as e:
|
|
729
|
+
order = self._rate_limit_failed_order(
|
|
730
|
+
oid=oid,
|
|
731
|
+
symbol=symbol,
|
|
732
|
+
side=side,
|
|
733
|
+
type=type,
|
|
734
|
+
amount=amount,
|
|
735
|
+
price=price,
|
|
736
|
+
time_in_force=time_in_force,
|
|
737
|
+
reduce_only=reduce_only,
|
|
738
|
+
exc=e,
|
|
739
|
+
)
|
|
740
|
+
self.order_status_update(order)
|
|
741
|
+
else:
|
|
742
|
+
try:
|
|
743
|
+
await self._ws_api_client.spot_place_order(id=oid, **params)
|
|
744
|
+
except BitgetRateLimitError as e:
|
|
745
|
+
order = self._rate_limit_failed_order(
|
|
746
|
+
oid=oid,
|
|
747
|
+
symbol=symbol,
|
|
748
|
+
side=side,
|
|
749
|
+
type=type,
|
|
750
|
+
amount=amount,
|
|
751
|
+
price=price,
|
|
752
|
+
time_in_force=time_in_force,
|
|
753
|
+
reduce_only=reduce_only,
|
|
754
|
+
exc=e,
|
|
755
|
+
)
|
|
756
|
+
self.order_status_update(order)
|
|
757
|
+
|
|
758
|
+
async def cancel_order_ws(self, oid: str, symbol: str, **kwargs) -> None:
|
|
759
|
+
market = self._market.get(symbol)
|
|
760
|
+
if not market:
|
|
761
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
762
|
+
instId = market.id
|
|
763
|
+
if self._account_type.is_uta:
|
|
764
|
+
await self._ws_api_client.uta_cancel_order(id=oid, clientOid=oid)
|
|
765
|
+
else:
|
|
766
|
+
if market.swap:
|
|
767
|
+
await self._ws_api_client.future_cancel_order(
|
|
768
|
+
id=oid,
|
|
769
|
+
instId=instId,
|
|
770
|
+
clientOid=oid,
|
|
771
|
+
instType=self._get_inst_type(market),
|
|
772
|
+
)
|
|
773
|
+
else:
|
|
774
|
+
await self._ws_api_client.spot_cancel_order(
|
|
775
|
+
id=oid, instId=instId, clientOid=oid
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
async def create_order(
|
|
779
|
+
self,
|
|
780
|
+
oid: str,
|
|
781
|
+
symbol: str,
|
|
782
|
+
side: OrderSide,
|
|
783
|
+
type: OrderType,
|
|
784
|
+
amount: Decimal,
|
|
785
|
+
price: Decimal | None,
|
|
786
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
787
|
+
reduce_only: bool = False,
|
|
788
|
+
**kwargs,
|
|
789
|
+
) -> Order:
|
|
790
|
+
"""Create an order"""
|
|
791
|
+
self._registry.register_tmp_order(
|
|
792
|
+
order=Order(
|
|
793
|
+
oid=oid,
|
|
794
|
+
exchange=self._exchange_id,
|
|
795
|
+
symbol=symbol,
|
|
796
|
+
status=OrderStatus.INITIALIZED,
|
|
797
|
+
amount=amount,
|
|
798
|
+
type=type,
|
|
799
|
+
side=side,
|
|
800
|
+
price=float(price) if price else None,
|
|
801
|
+
time_in_force=time_in_force,
|
|
802
|
+
timestamp=self._clock.timestamp_ms(),
|
|
803
|
+
reduce_only=reduce_only,
|
|
804
|
+
)
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
market = self._market.get(symbol)
|
|
808
|
+
if not market:
|
|
809
|
+
raise ValueError(f"Symbol {symbol} not found in market data.")
|
|
810
|
+
|
|
811
|
+
if self._account_type.is_uta:
|
|
812
|
+
category = kwargs.pop("category", self._get_inst_type(market))
|
|
813
|
+
params = {
|
|
814
|
+
"category": category,
|
|
815
|
+
"symbol": market.id,
|
|
816
|
+
"qty": str(amount),
|
|
817
|
+
"clientOid": oid,
|
|
818
|
+
}
|
|
819
|
+
if type.is_limit:
|
|
820
|
+
if price is None or time_in_force is None:
|
|
821
|
+
raise ValueError(
|
|
822
|
+
"price and time_in_force are required for limit orders"
|
|
823
|
+
)
|
|
824
|
+
params["price"] = str(price)
|
|
825
|
+
params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
826
|
+
time_in_force
|
|
827
|
+
).value
|
|
828
|
+
params["orderType"] = "limit"
|
|
829
|
+
elif type.is_post_only:
|
|
830
|
+
if price is None:
|
|
831
|
+
raise ValueError("price is required for post-only orders")
|
|
832
|
+
params["price"] = str(price)
|
|
833
|
+
params["timeInForce"] = "post_only"
|
|
834
|
+
params["orderType"] = "limit"
|
|
835
|
+
elif type.is_market:
|
|
836
|
+
bookl1 = self._cache.bookl1(symbol)
|
|
837
|
+
if not bookl1:
|
|
838
|
+
raise ValueError(
|
|
839
|
+
"Please Subscribe to bookl1 first, since market order requires bookl1 data"
|
|
840
|
+
)
|
|
841
|
+
if side.is_buy:
|
|
842
|
+
price = self._price_to_precision(
|
|
843
|
+
symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
|
|
844
|
+
)
|
|
845
|
+
else:
|
|
846
|
+
price = self._price_to_precision(
|
|
847
|
+
symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
|
|
848
|
+
)
|
|
849
|
+
params["price"] = str(price)
|
|
850
|
+
params["timeInForce"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
851
|
+
TimeInForce.IOC
|
|
852
|
+
).value
|
|
853
|
+
params["orderType"] = "limit"
|
|
854
|
+
|
|
855
|
+
if reduce_only:
|
|
856
|
+
params["reduceOnly"] = "yes"
|
|
857
|
+
|
|
858
|
+
params.update(kwargs)
|
|
859
|
+
|
|
860
|
+
try:
|
|
861
|
+
res = await self._api_client.post_api_v3_trade_place_order(**params)
|
|
862
|
+
|
|
863
|
+
order = Order(
|
|
864
|
+
exchange=self._exchange_id,
|
|
865
|
+
symbol=symbol,
|
|
866
|
+
eid=str(res.data.orderId),
|
|
867
|
+
oid=oid,
|
|
868
|
+
status=OrderStatus.PENDING,
|
|
869
|
+
side=side,
|
|
870
|
+
type=type,
|
|
871
|
+
price=float(price) if price is not None else None,
|
|
872
|
+
amount=amount,
|
|
873
|
+
time_in_force=time_in_force,
|
|
874
|
+
filled=Decimal(0),
|
|
875
|
+
remaining=amount,
|
|
876
|
+
reduce_only=reduce_only,
|
|
877
|
+
timestamp=res.requestTime,
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
except Exception as e:
|
|
881
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
882
|
+
self._log.error(
|
|
883
|
+
f"Error creating order: {error_msg} params: {str(params)}"
|
|
884
|
+
)
|
|
885
|
+
order = Order(
|
|
886
|
+
exchange=self._exchange_id,
|
|
887
|
+
symbol=symbol,
|
|
888
|
+
oid=oid,
|
|
889
|
+
status=OrderStatus.FAILED,
|
|
890
|
+
side=side,
|
|
891
|
+
type=type,
|
|
892
|
+
price=float(price) if price is not None else None,
|
|
893
|
+
amount=amount,
|
|
894
|
+
time_in_force=time_in_force,
|
|
895
|
+
filled=Decimal(0),
|
|
896
|
+
remaining=amount,
|
|
897
|
+
reduce_only=reduce_only,
|
|
898
|
+
timestamp=self._clock.timestamp_ms(),
|
|
899
|
+
reason=error_msg,
|
|
900
|
+
)
|
|
901
|
+
self.order_status_update(order)
|
|
902
|
+
else:
|
|
903
|
+
params = {
|
|
904
|
+
"symbol": market.id,
|
|
905
|
+
"side": BitgetEnumParser.to_bitget_order_side(side).value,
|
|
906
|
+
"size": str(amount),
|
|
907
|
+
"clientOid": oid,
|
|
908
|
+
}
|
|
909
|
+
if type.is_limit:
|
|
910
|
+
if price is None or time_in_force is None:
|
|
911
|
+
raise ValueError(
|
|
912
|
+
"price and time_in_force are required for limit orders"
|
|
913
|
+
)
|
|
914
|
+
params["price"] = str(price)
|
|
915
|
+
params["force"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
916
|
+
time_in_force
|
|
917
|
+
).value
|
|
918
|
+
params["orderType"] = "limit"
|
|
919
|
+
elif type.is_post_only:
|
|
920
|
+
if price is None:
|
|
921
|
+
raise ValueError("price is required for post-only orders")
|
|
922
|
+
params["price"] = str(price)
|
|
923
|
+
params["force"] = "post_only"
|
|
924
|
+
params["orderType"] = "limit"
|
|
925
|
+
elif type.is_market:
|
|
926
|
+
bookl1 = self._cache.bookl1(symbol)
|
|
927
|
+
if not bookl1:
|
|
928
|
+
raise ValueError(
|
|
929
|
+
"Please Subscribe to bookl1 first, since market order requires bookl1 data"
|
|
930
|
+
)
|
|
931
|
+
if side.is_buy:
|
|
932
|
+
price = self._price_to_precision(
|
|
933
|
+
symbol, bookl1.ask * (1 + self._max_slippage), mode="ceil"
|
|
934
|
+
)
|
|
935
|
+
else:
|
|
936
|
+
price = self._price_to_precision(
|
|
937
|
+
symbol, bookl1.bid * (1 - self._max_slippage), mode="floor"
|
|
938
|
+
)
|
|
939
|
+
params["price"] = str(price)
|
|
940
|
+
params["force"] = BitgetEnumParser.to_bitget_time_in_force(
|
|
941
|
+
TimeInForce.IOC
|
|
942
|
+
).value
|
|
943
|
+
params["orderType"] = "limit"
|
|
944
|
+
|
|
945
|
+
params.update(kwargs)
|
|
946
|
+
|
|
947
|
+
try:
|
|
948
|
+
if market.swap:
|
|
949
|
+
params["marginCoin"] = market.quote
|
|
950
|
+
params["marginMode"] = kwargs.get("marginMode", "crossed")
|
|
951
|
+
params["productType"] = self._get_inst_type(market)
|
|
952
|
+
if reduce_only:
|
|
953
|
+
params["reduceOnly"] = "YES"
|
|
954
|
+
|
|
955
|
+
res = await self._api_client.post_api_v2_mix_order_place_order(
|
|
956
|
+
**params
|
|
957
|
+
)
|
|
958
|
+
else:
|
|
959
|
+
res = await self._api_client.post_api_v2_spot_trade_place_order(
|
|
960
|
+
**params
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
order = Order(
|
|
964
|
+
exchange=self._exchange_id,
|
|
965
|
+
symbol=symbol,
|
|
966
|
+
oid=oid,
|
|
967
|
+
eid=str(res.data.orderId),
|
|
968
|
+
status=OrderStatus.PENDING,
|
|
969
|
+
side=side,
|
|
970
|
+
type=type,
|
|
971
|
+
price=float(price) if price is not None else None,
|
|
972
|
+
amount=amount,
|
|
973
|
+
time_in_force=time_in_force,
|
|
974
|
+
filled=Decimal(0),
|
|
975
|
+
remaining=amount,
|
|
976
|
+
reduce_only=reduce_only,
|
|
977
|
+
timestamp=res.requestTime,
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
except Exception as e:
|
|
981
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
982
|
+
self._log.error(
|
|
983
|
+
f"Error creating order: {error_msg} params: {str(params)}"
|
|
984
|
+
)
|
|
985
|
+
order = Order(
|
|
986
|
+
exchange=self._exchange_id,
|
|
987
|
+
symbol=symbol,
|
|
988
|
+
oid=oid,
|
|
989
|
+
status=OrderStatus.FAILED,
|
|
990
|
+
side=side,
|
|
991
|
+
type=type,
|
|
992
|
+
price=float(price) if price is not None else None,
|
|
993
|
+
amount=amount,
|
|
994
|
+
time_in_force=time_in_force,
|
|
995
|
+
filled=Decimal(0),
|
|
996
|
+
remaining=amount,
|
|
997
|
+
reduce_only=reduce_only,
|
|
998
|
+
timestamp=self._clock.timestamp_ms(),
|
|
999
|
+
reason=error_msg,
|
|
1000
|
+
)
|
|
1001
|
+
self.order_status_update(order)
|
|
1002
|
+
return order
|
|
1003
|
+
|
|
1004
|
+
async def create_batch_orders(
|
|
1005
|
+
self,
|
|
1006
|
+
orders: List[BatchOrderSubmit],
|
|
1007
|
+
) -> List[Order]:
|
|
1008
|
+
"""Create multiple orders in a batch"""
|
|
1009
|
+
raise NotImplementedError
|
|
1010
|
+
|
|
1011
|
+
async def cancel_order(self, oid: str, symbol: str, **kwargs) -> Order:
|
|
1012
|
+
"""Cancel an order"""
|
|
1013
|
+
market = self._market.get(symbol)
|
|
1014
|
+
if not market:
|
|
1015
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1016
|
+
id = market.id
|
|
1017
|
+
params = {
|
|
1018
|
+
"symbol": id,
|
|
1019
|
+
"clientOid": oid,
|
|
1020
|
+
}
|
|
1021
|
+
params.update(kwargs)
|
|
1022
|
+
try:
|
|
1023
|
+
if self._account_type.is_uta:
|
|
1024
|
+
res = await self._api_client.post_api_v3_trade_cancel_order(
|
|
1025
|
+
clientOid=oid,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
else:
|
|
1029
|
+
if market.swap:
|
|
1030
|
+
params["productType"] = self._get_inst_type(market)
|
|
1031
|
+
res = await self._api_client.post_api_v2_mix_order_cancel_order(
|
|
1032
|
+
**params
|
|
1033
|
+
)
|
|
1034
|
+
else:
|
|
1035
|
+
res = await self._api_client.post_api_v2_spot_trade_cancel_order(
|
|
1036
|
+
**params
|
|
1037
|
+
)
|
|
1038
|
+
order = Order(
|
|
1039
|
+
oid=oid,
|
|
1040
|
+
exchange=self._exchange_id,
|
|
1041
|
+
eid=res.data.orderId,
|
|
1042
|
+
timestamp=res.requestTime,
|
|
1043
|
+
symbol=symbol,
|
|
1044
|
+
status=OrderStatus.CANCELING,
|
|
1045
|
+
)
|
|
1046
|
+
except BitgetRateLimitError as e:
|
|
1047
|
+
error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s): {str(e)}"
|
|
1048
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
1049
|
+
order = Order(
|
|
1050
|
+
oid=oid,
|
|
1051
|
+
exchange=self._exchange_id,
|
|
1052
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1053
|
+
symbol=symbol,
|
|
1054
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1055
|
+
reason=error_msg,
|
|
1056
|
+
)
|
|
1057
|
+
except BitgetError as e:
|
|
1058
|
+
error_msg = str(e)
|
|
1059
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
1060
|
+
order = Order(
|
|
1061
|
+
oid=oid,
|
|
1062
|
+
exchange=self._exchange_id,
|
|
1063
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1064
|
+
symbol=symbol,
|
|
1065
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1066
|
+
reason=error_msg,
|
|
1067
|
+
)
|
|
1068
|
+
except Exception as e:
|
|
1069
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1070
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
1071
|
+
order = Order(
|
|
1072
|
+
oid=oid,
|
|
1073
|
+
exchange=self._exchange_id,
|
|
1074
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1075
|
+
symbol=symbol,
|
|
1076
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1077
|
+
reason=error_msg,
|
|
1078
|
+
)
|
|
1079
|
+
self.order_status_update(order)
|
|
1080
|
+
return order
|
|
1081
|
+
|
|
1082
|
+
@staticmethod
|
|
1083
|
+
def _optional_decimal(value: Any) -> Decimal | None:
|
|
1084
|
+
if value is None or value == "":
|
|
1085
|
+
return None
|
|
1086
|
+
return Decimal(str(value))
|
|
1087
|
+
|
|
1088
|
+
@staticmethod
|
|
1089
|
+
def _optional_float(value: Any) -> float | None:
|
|
1090
|
+
if value is None or value == "":
|
|
1091
|
+
return None
|
|
1092
|
+
return float(value)
|
|
1093
|
+
|
|
1094
|
+
@staticmethod
|
|
1095
|
+
def _parse_reduce_only(value: Any) -> bool | None:
|
|
1096
|
+
if value is None or value == "":
|
|
1097
|
+
return None
|
|
1098
|
+
if isinstance(value, bool):
|
|
1099
|
+
return value
|
|
1100
|
+
return str(value).lower() in {"yes", "true", "1"}
|
|
1101
|
+
|
|
1102
|
+
def _parse_query_order_data(
|
|
1103
|
+
self,
|
|
1104
|
+
*,
|
|
1105
|
+
oid: str,
|
|
1106
|
+
symbol: str,
|
|
1107
|
+
data: dict[str, Any],
|
|
1108
|
+
) -> Order:
|
|
1109
|
+
cached_order = self._cache.get_order(oid).value_or(None)
|
|
1110
|
+
|
|
1111
|
+
status_value = (
|
|
1112
|
+
data.get("orderStatus") or data.get("state") or data.get("status")
|
|
1113
|
+
)
|
|
1114
|
+
status = BitgetEnumParser.parse_order_status(
|
|
1115
|
+
BitgetOrderStatus(str(status_value))
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
tif_value = data.get("timeInForce") or data.get("force")
|
|
1119
|
+
time_in_force = None
|
|
1120
|
+
if tif_value:
|
|
1121
|
+
tif = BitgetTimeInForce(str(tif_value).lower().replace(" ", "_"))
|
|
1122
|
+
time_in_force = (
|
|
1123
|
+
TimeInForce.GTC
|
|
1124
|
+
if tif.is_post_only
|
|
1125
|
+
else (BitgetEnumParser.parse_time_in_force(tif))
|
|
1126
|
+
)
|
|
1127
|
+
|
|
1128
|
+
if cached_order and cached_order.type:
|
|
1129
|
+
order_type = cached_order.type
|
|
1130
|
+
else:
|
|
1131
|
+
order_type_value = data.get("orderType")
|
|
1132
|
+
if tif_value and str(tif_value).lower().replace(" ", "_") == "post_only":
|
|
1133
|
+
order_type = OrderType.POST_ONLY
|
|
1134
|
+
else:
|
|
1135
|
+
order_type = BitgetEnumParser.parse_order_type(
|
|
1136
|
+
BitgetOrderType(str(order_type_value))
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
amount = self._optional_decimal(data.get("qty"))
|
|
1140
|
+
if amount is None:
|
|
1141
|
+
amount = self._optional_decimal(data.get("size"))
|
|
1142
|
+
if (amount is None or amount == Decimal(0)) and cached_order:
|
|
1143
|
+
amount = cached_order.amount
|
|
1144
|
+
|
|
1145
|
+
filled = (
|
|
1146
|
+
self._optional_decimal(data.get("cumExecQty"))
|
|
1147
|
+
or self._optional_decimal(data.get("baseVolume"))
|
|
1148
|
+
or Decimal(0)
|
|
1149
|
+
)
|
|
1150
|
+
remaining = amount - filled if amount is not None else None
|
|
1151
|
+
if remaining is not None and remaining < Decimal(0):
|
|
1152
|
+
remaining = None
|
|
1153
|
+
|
|
1154
|
+
fee = None
|
|
1155
|
+
fee_currency = None
|
|
1156
|
+
fee_detail = data.get("feeDetail")
|
|
1157
|
+
if isinstance(fee_detail, list) and fee_detail:
|
|
1158
|
+
fee = sum(
|
|
1159
|
+
(Decimal(str(item.get("fee", "0"))) for item in fee_detail),
|
|
1160
|
+
Decimal(0),
|
|
1161
|
+
)
|
|
1162
|
+
fee_currency = fee_detail[0].get("feeCoin")
|
|
1163
|
+
elif data.get("fee"):
|
|
1164
|
+
fee = Decimal(str(data["fee"]))
|
|
1165
|
+
|
|
1166
|
+
position_side = None
|
|
1167
|
+
pos_side = data.get("posSide") or data.get("holdSide")
|
|
1168
|
+
if pos_side:
|
|
1169
|
+
position_side = BitgetPositionSide(str(pos_side)).parse_to_position_side()
|
|
1170
|
+
|
|
1171
|
+
timestamp = (
|
|
1172
|
+
data.get("updatedTime")
|
|
1173
|
+
or data.get("uTime")
|
|
1174
|
+
or data.get("createdTime")
|
|
1175
|
+
or data.get("cTime")
|
|
1176
|
+
or self._clock.timestamp_ms()
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
return Order(
|
|
1180
|
+
exchange=self._exchange_id,
|
|
1181
|
+
symbol=symbol,
|
|
1182
|
+
status=status,
|
|
1183
|
+
eid=str(data.get("orderId")) if data.get("orderId") else None,
|
|
1184
|
+
oid=str(data.get("clientOid") or oid),
|
|
1185
|
+
amount=amount,
|
|
1186
|
+
filled=filled,
|
|
1187
|
+
timestamp=int(timestamp),
|
|
1188
|
+
type=order_type,
|
|
1189
|
+
side=BitgetEnumParser.parse_order_side(BitgetOrderSide(str(data["side"])))
|
|
1190
|
+
if data.get("side")
|
|
1191
|
+
else None,
|
|
1192
|
+
time_in_force=time_in_force,
|
|
1193
|
+
price=self._optional_float(data.get("price")),
|
|
1194
|
+
average=self._optional_float(data.get("avgPrice") or data.get("priceAvg")),
|
|
1195
|
+
remaining=remaining,
|
|
1196
|
+
fee=fee,
|
|
1197
|
+
fee_currency=fee_currency,
|
|
1198
|
+
cum_cost=self._optional_decimal(
|
|
1199
|
+
data.get("cumExecValue") or data.get("quoteVolume")
|
|
1200
|
+
),
|
|
1201
|
+
reduce_only=self._parse_reduce_only(data.get("reduceOnly")),
|
|
1202
|
+
position_side=position_side,
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
async def query_order(self, oid: str, symbol: str) -> Order | None:
|
|
1206
|
+
market = self._market.get(symbol)
|
|
1207
|
+
if not market:
|
|
1208
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1209
|
+
|
|
1210
|
+
try:
|
|
1211
|
+
if self._account_type.is_uta:
|
|
1212
|
+
res = await self._api_client.get_api_v3_trade_order_info(clientOid=oid)
|
|
1213
|
+
elif market.swap:
|
|
1214
|
+
res = await self._api_client.get_api_v2_mix_order_detail(
|
|
1215
|
+
symbol=market.id,
|
|
1216
|
+
productType=self._get_inst_type(market),
|
|
1217
|
+
clientOid=oid,
|
|
1218
|
+
)
|
|
1219
|
+
else:
|
|
1220
|
+
res = await self._api_client.get_api_v2_spot_trade_order_info(
|
|
1221
|
+
clientOid=oid
|
|
1222
|
+
)
|
|
1223
|
+
|
|
1224
|
+
if res.get("code") != "00000":
|
|
1225
|
+
self._log.error(f"Error querying order: {res} oid={oid}")
|
|
1226
|
+
return None
|
|
1227
|
+
|
|
1228
|
+
data = res.get("data")
|
|
1229
|
+
if isinstance(data, list):
|
|
1230
|
+
data = data[0] if data else None
|
|
1231
|
+
if not data:
|
|
1232
|
+
return None
|
|
1233
|
+
|
|
1234
|
+
return self._parse_query_order_data(
|
|
1235
|
+
oid=oid,
|
|
1236
|
+
symbol=symbol,
|
|
1237
|
+
data=data,
|
|
1238
|
+
)
|
|
1239
|
+
except Exception as e:
|
|
1240
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1241
|
+
self._log.error(f"Error querying order: {error_msg} oid={oid}")
|
|
1242
|
+
return None
|
|
1243
|
+
|
|
1244
|
+
async def modify_order(
|
|
1245
|
+
self,
|
|
1246
|
+
oid: str,
|
|
1247
|
+
symbol: str,
|
|
1248
|
+
side: OrderSide | None = None,
|
|
1249
|
+
price: Decimal | None = None,
|
|
1250
|
+
amount: Decimal | None = None,
|
|
1251
|
+
**kwargs,
|
|
1252
|
+
) -> Order:
|
|
1253
|
+
"""Modify an order"""
|
|
1254
|
+
raise NotImplementedError
|
|
1255
|
+
|
|
1256
|
+
async def cancel_all_orders(self, symbol: str) -> bool:
|
|
1257
|
+
"""Cancel all orders"""
|
|
1258
|
+
if not self._account_type.is_uta:
|
|
1259
|
+
raise NotImplementedError("Only UTA account type is supported")
|
|
1260
|
+
|
|
1261
|
+
try:
|
|
1262
|
+
market = self._market.get(symbol)
|
|
1263
|
+
if not market:
|
|
1264
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1265
|
+
symbol = market.id
|
|
1266
|
+
category = self._get_inst_type(market)
|
|
1267
|
+
|
|
1268
|
+
await self._api_client.post_api_v3_trade_cancel_symbol_order(
|
|
1269
|
+
symbol=symbol, category=category
|
|
1270
|
+
)
|
|
1271
|
+
return True
|
|
1272
|
+
|
|
1273
|
+
except Exception as e:
|
|
1274
|
+
error_msg = f"{e.__class__.__name__}: {str(e)} params: symbol={symbol} category={category}"
|
|
1275
|
+
self._log.error(f"Error canceling all orders: {error_msg}")
|
|
1276
|
+
return False
|
|
1277
|
+
|
|
1278
|
+
def _ws_uta_msg_handler(self, raw: bytes):
|
|
1279
|
+
# if raw == b"pong":
|
|
1280
|
+
# self._ws_client._transport.notify_user_specific_pong_received()
|
|
1281
|
+
# self._log.debug(f"Pong received: `{raw.decode()}`")
|
|
1282
|
+
# return
|
|
1283
|
+
try:
|
|
1284
|
+
ws_msg = self._ws_msg_uta_general_decoder.decode(raw)
|
|
1285
|
+
if ws_msg.is_event_data:
|
|
1286
|
+
self._handle_event_data(ws_msg)
|
|
1287
|
+
elif ws_msg.arg is not None and ws_msg.arg.topic == "order":
|
|
1288
|
+
self._handle_uta_order_event(raw)
|
|
1289
|
+
elif ws_msg.arg is not None and ws_msg.arg.topic == "position":
|
|
1290
|
+
self._handle_uta_position_event(raw)
|
|
1291
|
+
elif ws_msg.arg is not None and ws_msg.arg.topic == "account":
|
|
1292
|
+
self._handle_uta_account_event(raw)
|
|
1293
|
+
|
|
1294
|
+
except msgspec.DecodeError as e:
|
|
1295
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
1296
|
+
|
|
1297
|
+
def _handle_uta_account_event(self, raw: bytes):
|
|
1298
|
+
msg = self._ws_msg_uta_account_decoder.decode(raw)
|
|
1299
|
+
balances = msg.parse_to_balances()
|
|
1300
|
+
self._cache._apply_balance(
|
|
1301
|
+
account_type=self._account_type,
|
|
1302
|
+
balances=balances,
|
|
1303
|
+
)
|
|
1304
|
+
for balance in balances:
|
|
1305
|
+
self._log.debug(
|
|
1306
|
+
f"Balance update: {balance.asset} - {balance.free} free, {balance.locked} locked"
|
|
1307
|
+
)
|
|
1308
|
+
|
|
1309
|
+
def _handle_uta_position_event(self, raw: bytes):
|
|
1310
|
+
msg = self._ws_msg_uta_positions_decoder.decode(raw)
|
|
1311
|
+
for data in msg.data:
|
|
1312
|
+
# Determine suffix based on marginCoin
|
|
1313
|
+
if data.marginCoin in ["USDT", "USDC"]:
|
|
1314
|
+
suffix = "linear"
|
|
1315
|
+
else:
|
|
1316
|
+
suffix = "inverse"
|
|
1317
|
+
|
|
1318
|
+
sym_id = f"{data.symbol}_{suffix}"
|
|
1319
|
+
symbol = self._market_id.get(sym_id)
|
|
1320
|
+
|
|
1321
|
+
if not symbol:
|
|
1322
|
+
self._log.warning(f"Symbol not found for UTA position: {sym_id}")
|
|
1323
|
+
continue
|
|
1324
|
+
|
|
1325
|
+
# Parse position side
|
|
1326
|
+
position_side = data.posSide.parse_to_position_side()
|
|
1327
|
+
signed_amount = Decimal(data.size)
|
|
1328
|
+
if position_side.is_short:
|
|
1329
|
+
signed_amount = -signed_amount
|
|
1330
|
+
|
|
1331
|
+
# Create Position object
|
|
1332
|
+
position = Position(
|
|
1333
|
+
symbol=symbol,
|
|
1334
|
+
exchange=self._exchange_id,
|
|
1335
|
+
signed_amount=signed_amount,
|
|
1336
|
+
entry_price=float(data.openPriceAvg or 0.0),
|
|
1337
|
+
side=position_side,
|
|
1338
|
+
unrealized_pnl=float(data.unrealisedPnl or 0.0),
|
|
1339
|
+
realized_pnl=float(data.curRealisedPnl or 0.0),
|
|
1340
|
+
)
|
|
1341
|
+
|
|
1342
|
+
# Apply position to cache
|
|
1343
|
+
self._cache._apply_position(position)
|
|
1344
|
+
self._log.debug(f"Position update: {str(position)}")
|
|
1345
|
+
|
|
1346
|
+
def _handle_uta_order_event(self, raw: bytes):
|
|
1347
|
+
msg = self._ws_msg_uta_orders_decoder.decode(raw)
|
|
1348
|
+
self._log.debug(f"Received UTA order event: {str(msg)}")
|
|
1349
|
+
for data in msg.data:
|
|
1350
|
+
tmp_order = self._registry.get_tmp_order(str(data.clientOid))
|
|
1351
|
+
if not tmp_order:
|
|
1352
|
+
continue
|
|
1353
|
+
|
|
1354
|
+
status = BitgetEnumParser.parse_order_status(data.orderStatus)
|
|
1355
|
+
if not status:
|
|
1356
|
+
continue
|
|
1357
|
+
|
|
1358
|
+
# Build symbol ID using the helper method
|
|
1359
|
+
inst_type_suffix = self._uta_inst_type_suffix(data.category)
|
|
1360
|
+
sym_id = f"{data.symbol}_{inst_type_suffix}"
|
|
1361
|
+
|
|
1362
|
+
symbol = self._market_id.get(sym_id)
|
|
1363
|
+
if not symbol:
|
|
1364
|
+
self._log.warning(f"Symbol not found for {sym_id}")
|
|
1365
|
+
continue
|
|
1366
|
+
|
|
1367
|
+
# Parse order data
|
|
1368
|
+
timestamp = int(data.updatedTime)
|
|
1369
|
+
|
|
1370
|
+
# NOTE: since market order is sent as limit order with taker price,
|
|
1371
|
+
# though it is a market order, the ws data will show it as limit order
|
|
1372
|
+
# we rely on the tmp_order to get the correct order type
|
|
1373
|
+
|
|
1374
|
+
# Parse order type
|
|
1375
|
+
# if data.orderType.is_market:
|
|
1376
|
+
# order_type = OrderType.MARKET
|
|
1377
|
+
# elif data.timeInForce.is_post_only:
|
|
1378
|
+
# order_type = OrderType.POST_ONLY
|
|
1379
|
+
# else:
|
|
1380
|
+
# order_type = OrderType.LIMIT # Default fallback
|
|
1381
|
+
order_type = tmp_order.type
|
|
1382
|
+
|
|
1383
|
+
# Calculate remaining quantity
|
|
1384
|
+
remaining = Decimal(data.qty) - Decimal(data.cumExecQty)
|
|
1385
|
+
|
|
1386
|
+
# Calculate average price
|
|
1387
|
+
average_price = float(data.avgPrice or 0)
|
|
1388
|
+
price = float(data.price or 0)
|
|
1389
|
+
|
|
1390
|
+
# Calculate fee
|
|
1391
|
+
total_fee = Decimal("0")
|
|
1392
|
+
fee_currency = None
|
|
1393
|
+
if data.feeDetail:
|
|
1394
|
+
total_fee = sum(
|
|
1395
|
+
(Decimal(fee.fee) for fee in data.feeDetail),
|
|
1396
|
+
Decimal("0"),
|
|
1397
|
+
)
|
|
1398
|
+
if data.feeDetail:
|
|
1399
|
+
fee_currency = data.feeDetail[0].feeCoin
|
|
1400
|
+
|
|
1401
|
+
order = Order(
|
|
1402
|
+
exchange=self._exchange_id,
|
|
1403
|
+
eid=data.orderId,
|
|
1404
|
+
oid=data.clientOid,
|
|
1405
|
+
timestamp=timestamp,
|
|
1406
|
+
symbol=symbol,
|
|
1407
|
+
type=order_type,
|
|
1408
|
+
side=BitgetEnumParser.parse_order_side(data.side),
|
|
1409
|
+
price=price,
|
|
1410
|
+
average=average_price,
|
|
1411
|
+
amount=Decimal(data.qty),
|
|
1412
|
+
filled=Decimal(data.cumExecQty),
|
|
1413
|
+
remaining=remaining,
|
|
1414
|
+
status=status,
|
|
1415
|
+
fee=total_fee,
|
|
1416
|
+
fee_currency=fee_currency,
|
|
1417
|
+
cum_cost=Decimal(data.cumExecValue or 0),
|
|
1418
|
+
reduce_only=data.reduceOnly == "yes",
|
|
1419
|
+
)
|
|
1420
|
+
self._log.debug(f"Order update: {str(order)}")
|
|
1421
|
+
self.order_status_update(order)
|
|
1422
|
+
|
|
1423
|
+
def _ws_msg_handler(self, raw: bytes):
|
|
1424
|
+
"""Handle incoming WebSocket messages"""
|
|
1425
|
+
# Process the message based on its type
|
|
1426
|
+
# if raw == b"pong":
|
|
1427
|
+
# self._ws_client._transport.notify_user_specific_pong_received()
|
|
1428
|
+
# self._log.debug(f"Pong received: `{raw.decode()}`")
|
|
1429
|
+
# return
|
|
1430
|
+
|
|
1431
|
+
try:
|
|
1432
|
+
ws_msg = self._ws_msg_general_decoder.decode(raw)
|
|
1433
|
+
if ws_msg.is_event_data:
|
|
1434
|
+
self._handle_event_data(ws_msg)
|
|
1435
|
+
elif ws_msg.arg is not None and ws_msg.arg.channel == "orders":
|
|
1436
|
+
self._handle_orders_event(raw, ws_msg.arg)
|
|
1437
|
+
elif ws_msg.arg is not None and ws_msg.arg.channel == "positions":
|
|
1438
|
+
self._handle_positions_event(raw, ws_msg.arg)
|
|
1439
|
+
elif ws_msg.arg is not None and ws_msg.arg.channel == "account":
|
|
1440
|
+
self._handle_account_event(raw, ws_msg.arg)
|
|
1441
|
+
|
|
1442
|
+
except msgspec.DecodeError as e:
|
|
1443
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
1444
|
+
|
|
1445
|
+
def _handle_account_event(self, raw: bytes, arg: BitgetWsArgMsg):
|
|
1446
|
+
if arg.instType.is_spot:
|
|
1447
|
+
msg = self._ws_msg_spot_account_decoder.decode(raw)
|
|
1448
|
+
else:
|
|
1449
|
+
msg = self._ws_msg_futures_account_decoder.decode(raw)
|
|
1450
|
+
self._cache._apply_balance(
|
|
1451
|
+
account_type=self._account_type, balances=msg.parse_to_balances()
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
def _handle_positions_event(self, raw: bytes, arg: BitgetWsArgMsg):
|
|
1455
|
+
msg = self._ws_msg_positions_decoder.decode(raw)
|
|
1456
|
+
|
|
1457
|
+
# Get existing positions for this specific instrument type
|
|
1458
|
+
existing_positions = self._cache.get_all_positions(exchange=self._exchange_id)
|
|
1459
|
+
inst_type_suffix = self._inst_type_suffix(arg.instType)
|
|
1460
|
+
|
|
1461
|
+
# Filter existing positions to only include those from this instrument type
|
|
1462
|
+
if arg.instType.is_usdt_swap:
|
|
1463
|
+
existing_positions_for_inst_type = {
|
|
1464
|
+
symbol: pos
|
|
1465
|
+
for symbol, pos in existing_positions.items()
|
|
1466
|
+
if self._market[symbol].quote == "USDT"
|
|
1467
|
+
}
|
|
1468
|
+
elif arg.instType.is_usdc_swap:
|
|
1469
|
+
existing_positions_for_inst_type = {
|
|
1470
|
+
symbol: pos
|
|
1471
|
+
for symbol, pos in existing_positions.items()
|
|
1472
|
+
if self._market[symbol].quote == "USDC"
|
|
1473
|
+
}
|
|
1474
|
+
elif arg.instType.is_inverse:
|
|
1475
|
+
existing_positions_for_inst_type = {
|
|
1476
|
+
symbol: pos
|
|
1477
|
+
for symbol, pos in existing_positions.items()
|
|
1478
|
+
if self._market[symbol].inverse
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
active_symbols = set()
|
|
1482
|
+
|
|
1483
|
+
for data in msg.data:
|
|
1484
|
+
sym_id = data.instId
|
|
1485
|
+
symbol = self._market_id[f"{sym_id}_{inst_type_suffix}"]
|
|
1486
|
+
active_symbols.add(symbol)
|
|
1487
|
+
|
|
1488
|
+
# Convert Bitget position data to Position
|
|
1489
|
+
signed_amount = Decimal(data.total)
|
|
1490
|
+
if data.holdSide.is_short:
|
|
1491
|
+
signed_amount = -signed_amount
|
|
1492
|
+
|
|
1493
|
+
position_side = data.holdSide.parse_to_position_side()
|
|
1494
|
+
|
|
1495
|
+
position = Position(
|
|
1496
|
+
symbol=symbol,
|
|
1497
|
+
exchange=self._exchange_id,
|
|
1498
|
+
signed_amount=signed_amount,
|
|
1499
|
+
entry_price=float(data.openPriceAvg),
|
|
1500
|
+
side=position_side,
|
|
1501
|
+
unrealized_pnl=float(data.unrealizedPL),
|
|
1502
|
+
realized_pnl=float(data.achievedProfits),
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
self._cache._apply_position(position)
|
|
1506
|
+
self._log.debug(f"Position update: {str(position)}")
|
|
1507
|
+
|
|
1508
|
+
# Close positions that are not in the snapshot (position goes to 0)
|
|
1509
|
+
# Only check positions for the current instrument type
|
|
1510
|
+
for symbol, existing_position in existing_positions_for_inst_type.items():
|
|
1511
|
+
if symbol not in active_symbols:
|
|
1512
|
+
# Create a closed position with signed_amount = 0
|
|
1513
|
+
closed_position = Position(
|
|
1514
|
+
symbol=symbol,
|
|
1515
|
+
exchange=self._exchange_id,
|
|
1516
|
+
signed_amount=Decimal("0"),
|
|
1517
|
+
entry_price=existing_position.entry_price,
|
|
1518
|
+
side=None,
|
|
1519
|
+
unrealized_pnl=0,
|
|
1520
|
+
realized_pnl=existing_position.realized_pnl,
|
|
1521
|
+
)
|
|
1522
|
+
self._cache._apply_position(closed_position)
|
|
1523
|
+
self._log.debug(f"Position closed: {str(closed_position)}")
|
|
1524
|
+
|
|
1525
|
+
def _handle_event_data(self, msg: BitgetWsGeneralMsg | BitgetWsUtaGeneralMsg):
|
|
1526
|
+
if msg.event == "subscribe":
|
|
1527
|
+
arg = msg.arg
|
|
1528
|
+
if arg is None:
|
|
1529
|
+
return
|
|
1530
|
+
self._log.debug(f"Subscribed to {arg.message}")
|
|
1531
|
+
elif msg.event == "error":
|
|
1532
|
+
code = msg.code
|
|
1533
|
+
error_msg = msg.msg
|
|
1534
|
+
self._log.error(f"Subscribed error code={code} {error_msg}")
|
|
1535
|
+
elif msg.event == "login":
|
|
1536
|
+
if msg.code == 0:
|
|
1537
|
+
self._log.debug("WebSocket login successful")
|
|
1538
|
+
else:
|
|
1539
|
+
self._log.error(f"WebSocket login failed: {msg.msg}")
|
|
1540
|
+
|
|
1541
|
+
def _handle_orders_event(self, raw: bytes, arg: BitgetWsArgMsg):
|
|
1542
|
+
msg = self._ws_msg_orders_decoder.decode(raw)
|
|
1543
|
+
self._log.debug(f"Received order event: {str(msg)}")
|
|
1544
|
+
for data in msg.data:
|
|
1545
|
+
tmp_order = self._registry.get_tmp_order(str(data.clientOid))
|
|
1546
|
+
if not tmp_order:
|
|
1547
|
+
continue
|
|
1548
|
+
|
|
1549
|
+
sym_id = data.instId
|
|
1550
|
+
timestamp = int(data.uTime)
|
|
1551
|
+
typ = tmp_order.type
|
|
1552
|
+
|
|
1553
|
+
status = BitgetEnumParser.parse_order_status(data.status)
|
|
1554
|
+
side = BitgetEnumParser.parse_order_side(data.side)
|
|
1555
|
+
|
|
1556
|
+
if fee_details := data.feeDetail:
|
|
1557
|
+
fee = fee_details[0].fee
|
|
1558
|
+
fee_currency = fee_details[0].feeCoin
|
|
1559
|
+
else:
|
|
1560
|
+
fee = 0
|
|
1561
|
+
fee_currency = None
|
|
1562
|
+
filled = data.accBaseVolume or "0"
|
|
1563
|
+
last_filled = data.accBaseVolume or "0"
|
|
1564
|
+
last_filled_price = data.fillPrice or 0
|
|
1565
|
+
price = data.price or 0
|
|
1566
|
+
average = data.priceAvg or 0
|
|
1567
|
+
new_size = data.newSize or "0"
|
|
1568
|
+
size = data.size or "0"
|
|
1569
|
+
|
|
1570
|
+
if arg.instType.is_spot:
|
|
1571
|
+
symbol = self._market_id[f"{sym_id}_spot"]
|
|
1572
|
+
order = Order(
|
|
1573
|
+
exchange=self._exchange_id,
|
|
1574
|
+
symbol=symbol,
|
|
1575
|
+
eid=str(data.orderId),
|
|
1576
|
+
oid=str(data.clientOid),
|
|
1577
|
+
amount=Decimal(new_size),
|
|
1578
|
+
filled=Decimal(filled),
|
|
1579
|
+
timestamp=timestamp,
|
|
1580
|
+
status=status,
|
|
1581
|
+
side=side,
|
|
1582
|
+
type=typ,
|
|
1583
|
+
last_filled=Decimal(last_filled),
|
|
1584
|
+
last_filled_price=float(last_filled_price),
|
|
1585
|
+
remaining=Decimal(new_size) - Decimal(filled),
|
|
1586
|
+
fee=Decimal(fee),
|
|
1587
|
+
fee_currency=fee_currency,
|
|
1588
|
+
price=float(price),
|
|
1589
|
+
average=float(average),
|
|
1590
|
+
cost=Decimal(last_filled) * Decimal(last_filled_price),
|
|
1591
|
+
cum_cost=Decimal(filled) * Decimal(average),
|
|
1592
|
+
)
|
|
1593
|
+
else:
|
|
1594
|
+
symbol = self._market_id[
|
|
1595
|
+
f"{sym_id}_{self._inst_type_suffix(arg.instType)}"
|
|
1596
|
+
]
|
|
1597
|
+
order = Order(
|
|
1598
|
+
exchange=self._exchange_id,
|
|
1599
|
+
symbol=symbol,
|
|
1600
|
+
eid=str(data.orderId),
|
|
1601
|
+
oid=str(data.clientOid),
|
|
1602
|
+
amount=Decimal(size),
|
|
1603
|
+
filled=Decimal(filled),
|
|
1604
|
+
timestamp=timestamp,
|
|
1605
|
+
status=status,
|
|
1606
|
+
side=side,
|
|
1607
|
+
type=typ,
|
|
1608
|
+
last_filled=Decimal(last_filled),
|
|
1609
|
+
last_filled_price=float(last_filled_price),
|
|
1610
|
+
remaining=Decimal(size) - Decimal(filled),
|
|
1611
|
+
fee=Decimal(fee),
|
|
1612
|
+
fee_currency=fee_currency,
|
|
1613
|
+
average=float(average),
|
|
1614
|
+
price=float(price),
|
|
1615
|
+
cost=Decimal(last_filled) * Decimal(last_filled_price),
|
|
1616
|
+
cum_cost=Decimal(filled) * Decimal(average),
|
|
1617
|
+
reduce_only=data.reduceOnly == "yes",
|
|
1618
|
+
)
|
|
1619
|
+
self.order_status_update(order)
|