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,1807 @@
|
|
|
1
|
+
import msgspec
|
|
2
|
+
import asyncio
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Mapping, List
|
|
6
|
+
from walrasquant.constants import (
|
|
7
|
+
PositionSide,
|
|
8
|
+
ExchangeType,
|
|
9
|
+
OrderSide,
|
|
10
|
+
OrderStatus,
|
|
11
|
+
OrderType,
|
|
12
|
+
TimeInForce,
|
|
13
|
+
TriggerType,
|
|
14
|
+
)
|
|
15
|
+
from walrasquant.config import OrderQueryConfig
|
|
16
|
+
from walrasquant.schema import Order, Position, BatchOrderSubmit, CancelOrderSubmit
|
|
17
|
+
from walrasquant.base import OrderManagementSystem
|
|
18
|
+
from walrasquant.core.registry import OrderRegistry
|
|
19
|
+
from walrasquant.core.entity import TaskManager
|
|
20
|
+
from walrasquant.error import PositionModeError
|
|
21
|
+
from walrasquant.exchange.binance.error import (
|
|
22
|
+
BinanceRateLimitError,
|
|
23
|
+
BinanceClientError,
|
|
24
|
+
BinanceServerError,
|
|
25
|
+
)
|
|
26
|
+
from walrasquant.exchange.binance.schema import (
|
|
27
|
+
BinanceMarket,
|
|
28
|
+
BinanceOrder,
|
|
29
|
+
BinanceFuturesPositionInfo,
|
|
30
|
+
BinancePortfolioMarginPositionRisk,
|
|
31
|
+
)
|
|
32
|
+
from walrasquant.exchange.binance.constants import (
|
|
33
|
+
BinanceAccountType,
|
|
34
|
+
BinanceOrderType,
|
|
35
|
+
BinanceTimeInForce,
|
|
36
|
+
)
|
|
37
|
+
from walrasquant.exchange.binance.rest_api import BinanceApiClient
|
|
38
|
+
from walrasquant.exchange.binance.websockets import BinanceWSClient, BinanceWSApiClient
|
|
39
|
+
from walrasquant.core.nautilius_core import LiveClock, MessageBus
|
|
40
|
+
from walrasquant.exchange.binance.constants import (
|
|
41
|
+
BinanceUserDataStreamWsEventType,
|
|
42
|
+
BinanceBusinessUnit,
|
|
43
|
+
BinanceEnumParser,
|
|
44
|
+
)
|
|
45
|
+
from walrasquant.exchange.binance.schema import (
|
|
46
|
+
BinanceUserDataStreamMsg,
|
|
47
|
+
BinanceSpotOrderUpdateMsg,
|
|
48
|
+
_BinanceSpotOrderUpdateMsg,
|
|
49
|
+
_BinanceSpotUpdateMsg,
|
|
50
|
+
BinanceFuturesOrderUpdateMsg,
|
|
51
|
+
BinanceWsOrderResponse,
|
|
52
|
+
BinanceSpotUpdateMsg,
|
|
53
|
+
BinanceFuturesUpdateMsg,
|
|
54
|
+
BinancePortfolioMarginBalance,
|
|
55
|
+
BinanceSpotUserDataStreamMsg,
|
|
56
|
+
)
|
|
57
|
+
from walrasquant.core.cache import AsyncCache
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class BinanceOrderManagementSystem(OrderManagementSystem):
|
|
61
|
+
_account_type: BinanceAccountType
|
|
62
|
+
_market: Mapping[str, BinanceMarket]
|
|
63
|
+
_market_id: Dict[str, str]
|
|
64
|
+
_api_client: BinanceApiClient
|
|
65
|
+
_ws_client: BinanceWSClient
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
account_type: BinanceAccountType,
|
|
70
|
+
api_key: str,
|
|
71
|
+
secret: str,
|
|
72
|
+
market: Dict[str, BinanceMarket],
|
|
73
|
+
market_id: Dict[str, str],
|
|
74
|
+
registry: OrderRegistry,
|
|
75
|
+
cache: AsyncCache,
|
|
76
|
+
api_client: BinanceApiClient,
|
|
77
|
+
exchange_id: ExchangeType,
|
|
78
|
+
clock: LiveClock,
|
|
79
|
+
msgbus: MessageBus,
|
|
80
|
+
task_manager: TaskManager,
|
|
81
|
+
enable_rate_limit: bool,
|
|
82
|
+
order_query_config: OrderQueryConfig,
|
|
83
|
+
max_subscriptions_per_client: int | None = None,
|
|
84
|
+
max_clients: int | None = None,
|
|
85
|
+
):
|
|
86
|
+
super().__init__(
|
|
87
|
+
account_type=account_type,
|
|
88
|
+
market=market, # type: ignore[arg-type]
|
|
89
|
+
market_id=market_id,
|
|
90
|
+
registry=registry,
|
|
91
|
+
cache=cache,
|
|
92
|
+
api_client=api_client,
|
|
93
|
+
ws_client=BinanceWSClient(
|
|
94
|
+
account_type=account_type,
|
|
95
|
+
handler=self._ws_msg_handler,
|
|
96
|
+
task_manager=task_manager,
|
|
97
|
+
clock=clock,
|
|
98
|
+
# For USD-M Futures, connect to the dedicated /private channel URL
|
|
99
|
+
custom_url=(
|
|
100
|
+
(account_type.ws_private_url + "/ws")
|
|
101
|
+
if account_type.ws_private_url
|
|
102
|
+
else None
|
|
103
|
+
),
|
|
104
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
105
|
+
max_clients=max_clients,
|
|
106
|
+
)
|
|
107
|
+
if not account_type.is_spot
|
|
108
|
+
else BinanceWSApiClient(
|
|
109
|
+
account_type=account_type,
|
|
110
|
+
api_key=api_key,
|
|
111
|
+
secret=secret,
|
|
112
|
+
handler=self._ws_spot_msg_handler,
|
|
113
|
+
task_manager=task_manager,
|
|
114
|
+
clock=clock,
|
|
115
|
+
enable_rate_limit=enable_rate_limit,
|
|
116
|
+
),
|
|
117
|
+
exchange_id=exchange_id,
|
|
118
|
+
clock=clock,
|
|
119
|
+
msgbus=msgbus,
|
|
120
|
+
task_manager=task_manager,
|
|
121
|
+
order_query_config=order_query_config,
|
|
122
|
+
)
|
|
123
|
+
self._ws_api_client = None
|
|
124
|
+
if self._account_type.is_spot or self._account_type.is_future:
|
|
125
|
+
self._ws_api_client = BinanceWSApiClient(
|
|
126
|
+
account_type=account_type,
|
|
127
|
+
api_key=api_key,
|
|
128
|
+
secret=secret,
|
|
129
|
+
handler=self._ws_api_msg_handler,
|
|
130
|
+
task_manager=task_manager,
|
|
131
|
+
clock=clock,
|
|
132
|
+
enable_rate_limit=enable_rate_limit,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
self._ws_msg_general_decoder = msgspec.json.Decoder(BinanceUserDataStreamMsg)
|
|
136
|
+
self._ws_spot_msg_general_decoder = msgspec.json.Decoder(
|
|
137
|
+
BinanceSpotUserDataStreamMsg
|
|
138
|
+
)
|
|
139
|
+
self._ws_msg_spot_order_update_decoder = msgspec.json.Decoder(
|
|
140
|
+
BinanceSpotOrderUpdateMsg
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
self._ws_msg_pm_margin_order_update_decoder = msgspec.json.Decoder(
|
|
144
|
+
_BinanceSpotOrderUpdateMsg
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
self._ws_msg_pm_margin_account_update_decoder = msgspec.json.Decoder(
|
|
148
|
+
_BinanceSpotUpdateMsg
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
self._ws_msg_futures_order_update_decoder = msgspec.json.Decoder(
|
|
152
|
+
BinanceFuturesOrderUpdateMsg
|
|
153
|
+
)
|
|
154
|
+
self._ws_msg_spot_account_update_decoder = msgspec.json.Decoder(
|
|
155
|
+
BinanceSpotUpdateMsg
|
|
156
|
+
)
|
|
157
|
+
self._ws_msg_futures_account_update_decoder = msgspec.json.Decoder(
|
|
158
|
+
BinanceFuturesUpdateMsg
|
|
159
|
+
)
|
|
160
|
+
self._ws_msg_ws_api_response_decoder = msgspec.json.Decoder(
|
|
161
|
+
BinanceWsOrderResponse
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def market_type(self):
|
|
166
|
+
if self._account_type.is_spot:
|
|
167
|
+
return "_spot"
|
|
168
|
+
elif self._account_type.is_linear:
|
|
169
|
+
return "_linear"
|
|
170
|
+
elif self._account_type.is_inverse:
|
|
171
|
+
return "_inverse"
|
|
172
|
+
|
|
173
|
+
def _supports_query_market(self, market: BinanceMarket) -> bool:
|
|
174
|
+
if self._account_type.is_spot:
|
|
175
|
+
return market.spot
|
|
176
|
+
if self._account_type.is_isolated_margin_or_margin:
|
|
177
|
+
return market.margin
|
|
178
|
+
if self._account_type.is_linear:
|
|
179
|
+
return market.linear
|
|
180
|
+
if self._account_type.is_inverse:
|
|
181
|
+
return market.inverse
|
|
182
|
+
if self._account_type.is_portfolio_margin:
|
|
183
|
+
return market.margin or market.linear or market.inverse
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
def _ws_api_msg_handler(self, raw: bytes):
|
|
187
|
+
try:
|
|
188
|
+
msg = self._ws_msg_ws_api_response_decoder.decode(raw)
|
|
189
|
+
id = msg.id
|
|
190
|
+
oid = id[1:] # remove the prefix 'n' or 'c'
|
|
191
|
+
|
|
192
|
+
tmp_order = self._registry.get_tmp_order(oid)
|
|
193
|
+
if not tmp_order:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
ts = self._clock.timestamp_ms()
|
|
197
|
+
if id.startswith("n"): # new order
|
|
198
|
+
if msg.is_success:
|
|
199
|
+
assert msg.result is not None
|
|
200
|
+
sym_id = f"{msg.result.symbol}{self.market_type}"
|
|
201
|
+
symbol = self._market_id[sym_id]
|
|
202
|
+
eid = str(msg.result.orderId)
|
|
203
|
+
self._log.debug(
|
|
204
|
+
f"[{symbol}] new order success: oid: {oid} eid: {eid}"
|
|
205
|
+
)
|
|
206
|
+
order = Order(
|
|
207
|
+
exchange=self._exchange_id,
|
|
208
|
+
symbol=symbol,
|
|
209
|
+
oid=oid,
|
|
210
|
+
eid=eid,
|
|
211
|
+
status=OrderStatus.PENDING,
|
|
212
|
+
side=tmp_order.side,
|
|
213
|
+
timestamp=ts,
|
|
214
|
+
amount=tmp_order.amount,
|
|
215
|
+
type=tmp_order.type,
|
|
216
|
+
price=tmp_order.price,
|
|
217
|
+
time_in_force=tmp_order.time_in_force,
|
|
218
|
+
reduce_only=tmp_order.reduce_only,
|
|
219
|
+
)
|
|
220
|
+
self.order_status_update(order)
|
|
221
|
+
else:
|
|
222
|
+
assert msg.error is not None
|
|
223
|
+
symbol = tmp_order.symbol
|
|
224
|
+
self._log.error(
|
|
225
|
+
f"[{symbol}] new order failed: oid: {oid} {msg.error.format_str}"
|
|
226
|
+
)
|
|
227
|
+
order = Order(
|
|
228
|
+
exchange=self._exchange_id,
|
|
229
|
+
symbol=symbol,
|
|
230
|
+
oid=oid,
|
|
231
|
+
status=OrderStatus.FAILED,
|
|
232
|
+
amount=tmp_order.amount,
|
|
233
|
+
type=tmp_order.type,
|
|
234
|
+
timestamp=ts,
|
|
235
|
+
side=tmp_order.side,
|
|
236
|
+
price=tmp_order.price,
|
|
237
|
+
time_in_force=tmp_order.time_in_force,
|
|
238
|
+
reduce_only=tmp_order.reduce_only,
|
|
239
|
+
reason=msg.error.format_str,
|
|
240
|
+
)
|
|
241
|
+
self.order_status_update(order)
|
|
242
|
+
else:
|
|
243
|
+
if msg.is_success:
|
|
244
|
+
assert msg.result is not None
|
|
245
|
+
sym_id = f"{msg.result.symbol}{self.market_type}"
|
|
246
|
+
symbol = self._market_id[sym_id]
|
|
247
|
+
eid = str(msg.result.orderId)
|
|
248
|
+
self._log.debug(
|
|
249
|
+
f"[{symbol}] canceling order success: oid: {oid} eid: {eid}"
|
|
250
|
+
)
|
|
251
|
+
order = Order(
|
|
252
|
+
exchange=self._exchange_id,
|
|
253
|
+
symbol=symbol,
|
|
254
|
+
oid=oid,
|
|
255
|
+
eid=eid,
|
|
256
|
+
status=OrderStatus.CANCELING,
|
|
257
|
+
amount=tmp_order.amount,
|
|
258
|
+
side=tmp_order.side,
|
|
259
|
+
timestamp=ts,
|
|
260
|
+
type=tmp_order.type,
|
|
261
|
+
price=tmp_order.price,
|
|
262
|
+
time_in_force=tmp_order.time_in_force,
|
|
263
|
+
reduce_only=tmp_order.reduce_only,
|
|
264
|
+
)
|
|
265
|
+
self.order_status_update(order)
|
|
266
|
+
else:
|
|
267
|
+
assert msg.error is not None
|
|
268
|
+
self._log.error(
|
|
269
|
+
f"[{tmp_order.symbol}] canceling order failed: oid: {oid} {msg.error.format_str}"
|
|
270
|
+
)
|
|
271
|
+
order = Order(
|
|
272
|
+
exchange=self._exchange_id,
|
|
273
|
+
symbol=tmp_order.symbol,
|
|
274
|
+
oid=oid,
|
|
275
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
276
|
+
amount=tmp_order.amount,
|
|
277
|
+
type=tmp_order.type,
|
|
278
|
+
side=tmp_order.side,
|
|
279
|
+
timestamp=ts,
|
|
280
|
+
price=tmp_order.price,
|
|
281
|
+
time_in_force=tmp_order.time_in_force,
|
|
282
|
+
reduce_only=tmp_order.reduce_only,
|
|
283
|
+
reason=msg.error.format_str,
|
|
284
|
+
)
|
|
285
|
+
self.order_status_update(order) # SOME STATUS -> FAILED
|
|
286
|
+
|
|
287
|
+
except msgspec.DecodeError as e:
|
|
288
|
+
self._log.error(f"Error decoding WebSocket API message: {str(raw)} {e}")
|
|
289
|
+
|
|
290
|
+
def _ws_spot_msg_handler(self, raw: bytes):
|
|
291
|
+
try:
|
|
292
|
+
msg = self._ws_spot_msg_general_decoder.decode(raw)
|
|
293
|
+
if msg.event:
|
|
294
|
+
match msg.event.e:
|
|
295
|
+
case (
|
|
296
|
+
BinanceUserDataStreamWsEventType.EXECUTION_REPORT
|
|
297
|
+
): # spot order update
|
|
298
|
+
self._parse_execution_report(raw)
|
|
299
|
+
case (
|
|
300
|
+
BinanceUserDataStreamWsEventType.OUT_BOUND_ACCOUNT_POSITION
|
|
301
|
+
): # spot account update
|
|
302
|
+
self._parse_out_bound_account_position(raw)
|
|
303
|
+
|
|
304
|
+
except msgspec.DecodeError as e:
|
|
305
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
306
|
+
|
|
307
|
+
def _ws_msg_handler(self, raw: bytes):
|
|
308
|
+
try:
|
|
309
|
+
msg = self._ws_msg_general_decoder.decode(raw)
|
|
310
|
+
if msg.e:
|
|
311
|
+
match msg.e:
|
|
312
|
+
case (
|
|
313
|
+
BinanceUserDataStreamWsEventType.ORDER_TRADE_UPDATE
|
|
314
|
+
): # futures order update
|
|
315
|
+
self._parse_order_trade_update(raw)
|
|
316
|
+
case (
|
|
317
|
+
BinanceUserDataStreamWsEventType.EXECUTION_REPORT
|
|
318
|
+
): # spot order update
|
|
319
|
+
self._parse_pm_execution_report(raw)
|
|
320
|
+
case (
|
|
321
|
+
BinanceUserDataStreamWsEventType.ACCOUNT_UPDATE
|
|
322
|
+
): # futures account update
|
|
323
|
+
self._parse_account_update(raw)
|
|
324
|
+
case (
|
|
325
|
+
BinanceUserDataStreamWsEventType.OUT_BOUND_ACCOUNT_POSITION
|
|
326
|
+
): # spot account update
|
|
327
|
+
self._parse_pm_out_bound_account_position(raw)
|
|
328
|
+
case BinanceUserDataStreamWsEventType.LISTEN_KEY_EXPIRED:
|
|
329
|
+
res = self._ws_msg_general_decoder.decode(raw)
|
|
330
|
+
self._log.warning(f"Listen key expired: {res}")
|
|
331
|
+
except msgspec.DecodeError as e:
|
|
332
|
+
self._log.error(f"Error decoding message: {str(raw)} {e}")
|
|
333
|
+
|
|
334
|
+
def _parse_order_trade_update(self, raw: bytes):
|
|
335
|
+
res = self._ws_msg_futures_order_update_decoder.decode(raw)
|
|
336
|
+
self._log.debug(f"Order trade update: {res}")
|
|
337
|
+
|
|
338
|
+
event_data = res.o
|
|
339
|
+
event_unit = res.fs
|
|
340
|
+
|
|
341
|
+
# Only portfolio margin has "UM" and "CM" event business unit
|
|
342
|
+
if event_unit == BinanceBusinessUnit.UM:
|
|
343
|
+
id = event_data.s + "_linear"
|
|
344
|
+
symbol = self._market_id[id]
|
|
345
|
+
elif event_unit == BinanceBusinessUnit.CM:
|
|
346
|
+
id = event_data.s + "_inverse"
|
|
347
|
+
symbol = self._market_id[id]
|
|
348
|
+
else:
|
|
349
|
+
assert self.market_type is not None
|
|
350
|
+
id = event_data.s + self.market_type
|
|
351
|
+
symbol = self._market_id[id]
|
|
352
|
+
|
|
353
|
+
# we use the last filled quantity to calculate the cost, instead of the accumulated filled quantity
|
|
354
|
+
type = event_data.o
|
|
355
|
+
if type.is_market:
|
|
356
|
+
cost = Decimal(event_data.l) * Decimal(event_data.ap)
|
|
357
|
+
cum_cost = Decimal(event_data.z) * Decimal(event_data.ap)
|
|
358
|
+
elif type.is_limit:
|
|
359
|
+
price = Decimal(event_data.ap) or Decimal(
|
|
360
|
+
event_data.p
|
|
361
|
+
) # if average price is 0 or empty, use price
|
|
362
|
+
cost = Decimal(event_data.l) * price
|
|
363
|
+
cum_cost = Decimal(event_data.z) * price
|
|
364
|
+
|
|
365
|
+
order = Order(
|
|
366
|
+
exchange=self._exchange_id,
|
|
367
|
+
symbol=symbol,
|
|
368
|
+
status=BinanceEnumParser.parse_order_status(event_data.X),
|
|
369
|
+
eid=str(event_data.i),
|
|
370
|
+
oid=event_data.c,
|
|
371
|
+
amount=Decimal(event_data.q),
|
|
372
|
+
filled=Decimal(event_data.z),
|
|
373
|
+
timestamp=res.E,
|
|
374
|
+
type=BinanceEnumParser.parse_futures_order_type(event_data.o, event_data.f),
|
|
375
|
+
side=BinanceEnumParser.parse_order_side(event_data.S),
|
|
376
|
+
time_in_force=BinanceEnumParser.parse_time_in_force(event_data.f),
|
|
377
|
+
price=float(event_data.p),
|
|
378
|
+
average=float(event_data.ap),
|
|
379
|
+
last_filled_price=float(event_data.L),
|
|
380
|
+
last_filled=Decimal(event_data.l),
|
|
381
|
+
remaining=Decimal(event_data.q) - Decimal(event_data.z),
|
|
382
|
+
fee=Decimal(event_data.n) if event_data.n else None,
|
|
383
|
+
fee_currency=event_data.N,
|
|
384
|
+
cum_cost=cum_cost,
|
|
385
|
+
cost=cost,
|
|
386
|
+
reduce_only=event_data.R,
|
|
387
|
+
position_side=BinanceEnumParser.parse_position_side(event_data.ps),
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
self.order_status_update(order)
|
|
391
|
+
|
|
392
|
+
def _parse_execution_report(self, raw: bytes) -> None:
|
|
393
|
+
data = self._ws_msg_spot_order_update_decoder.decode(raw)
|
|
394
|
+
event_data = data.event
|
|
395
|
+
self._log.debug(f"Execution report: {event_data}")
|
|
396
|
+
|
|
397
|
+
market_type = self.market_type or "_spot"
|
|
398
|
+
id = event_data.s + market_type
|
|
399
|
+
symbol = self._market_id[id]
|
|
400
|
+
|
|
401
|
+
# Calculate average price only if filled amount is non-zero
|
|
402
|
+
average = (
|
|
403
|
+
float(event_data.Z) / float(event_data.z)
|
|
404
|
+
if float(event_data.z) != 0
|
|
405
|
+
else None
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
order = Order(
|
|
409
|
+
exchange=self._exchange_id,
|
|
410
|
+
symbol=symbol,
|
|
411
|
+
status=BinanceEnumParser.parse_order_status(event_data.X),
|
|
412
|
+
eid=str(event_data.i),
|
|
413
|
+
oid=event_data.c,
|
|
414
|
+
amount=Decimal(event_data.q),
|
|
415
|
+
filled=Decimal(event_data.z),
|
|
416
|
+
timestamp=event_data.E,
|
|
417
|
+
type=BinanceEnumParser.parse_spot_order_type(event_data.o),
|
|
418
|
+
side=BinanceEnumParser.parse_order_side(event_data.S),
|
|
419
|
+
time_in_force=BinanceEnumParser.parse_time_in_force(event_data.f),
|
|
420
|
+
price=float(event_data.p),
|
|
421
|
+
average=average,
|
|
422
|
+
last_filled_price=float(event_data.L),
|
|
423
|
+
last_filled=Decimal(event_data.l),
|
|
424
|
+
remaining=Decimal(event_data.q) - Decimal(event_data.z),
|
|
425
|
+
fee=Decimal(event_data.n) if event_data.n else None,
|
|
426
|
+
fee_currency=event_data.N,
|
|
427
|
+
cum_cost=Decimal(event_data.Z),
|
|
428
|
+
cost=Decimal(event_data.Y),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
self.order_status_update(order)
|
|
432
|
+
|
|
433
|
+
def _parse_pm_execution_report(self, raw: bytes) -> None:
|
|
434
|
+
event_data = self._ws_msg_pm_margin_order_update_decoder.decode(raw)
|
|
435
|
+
self._log.debug(f"Execution report: {event_data}")
|
|
436
|
+
|
|
437
|
+
market_type = self.market_type or "_spot"
|
|
438
|
+
id = event_data.s + market_type
|
|
439
|
+
symbol = self._market_id[id]
|
|
440
|
+
|
|
441
|
+
# Calculate average price only if filled amount is non-zero
|
|
442
|
+
average = (
|
|
443
|
+
float(event_data.Z) / float(event_data.z)
|
|
444
|
+
if float(event_data.z) != 0
|
|
445
|
+
else None
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
order = Order(
|
|
449
|
+
exchange=self._exchange_id,
|
|
450
|
+
symbol=symbol,
|
|
451
|
+
status=BinanceEnumParser.parse_order_status(event_data.X),
|
|
452
|
+
eid=str(event_data.i),
|
|
453
|
+
oid=event_data.c,
|
|
454
|
+
amount=Decimal(event_data.q),
|
|
455
|
+
filled=Decimal(event_data.z),
|
|
456
|
+
timestamp=event_data.E,
|
|
457
|
+
type=BinanceEnumParser.parse_spot_order_type(event_data.o),
|
|
458
|
+
side=BinanceEnumParser.parse_order_side(event_data.S),
|
|
459
|
+
time_in_force=BinanceEnumParser.parse_time_in_force(event_data.f),
|
|
460
|
+
price=float(event_data.p),
|
|
461
|
+
average=average,
|
|
462
|
+
last_filled_price=float(event_data.L),
|
|
463
|
+
last_filled=Decimal(event_data.l),
|
|
464
|
+
remaining=Decimal(event_data.q) - Decimal(event_data.z),
|
|
465
|
+
fee=Decimal(event_data.n) if event_data.n else None,
|
|
466
|
+
fee_currency=event_data.N,
|
|
467
|
+
cum_cost=Decimal(event_data.Z),
|
|
468
|
+
cost=Decimal(event_data.Y),
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
self.order_status_update(order)
|
|
472
|
+
|
|
473
|
+
def _parse_account_update(self, raw: bytes):
|
|
474
|
+
res = self._ws_msg_futures_account_update_decoder.decode(raw)
|
|
475
|
+
self._log.debug(f"Account update: {res}")
|
|
476
|
+
|
|
477
|
+
balances = res.parse_to_balances()
|
|
478
|
+
self._cache._apply_balance(account_type=self._account_type, balances=balances)
|
|
479
|
+
|
|
480
|
+
event_unit = res.fs
|
|
481
|
+
for position in res.a.P:
|
|
482
|
+
if event_unit == BinanceBusinessUnit.UM:
|
|
483
|
+
id = position.s + "_linear"
|
|
484
|
+
symbol = self._market_id[id]
|
|
485
|
+
elif event_unit == BinanceBusinessUnit.CM:
|
|
486
|
+
id = position.s + "_inverse"
|
|
487
|
+
symbol = self._market_id[id]
|
|
488
|
+
else:
|
|
489
|
+
assert self.market_type is not None
|
|
490
|
+
id = position.s + self.market_type
|
|
491
|
+
symbol = self._market_id[id]
|
|
492
|
+
|
|
493
|
+
signed_amount = Decimal(position.pa)
|
|
494
|
+
side = position.ps.parse_to_position_side()
|
|
495
|
+
if signed_amount == 0:
|
|
496
|
+
side = None # 0 means no position side
|
|
497
|
+
else:
|
|
498
|
+
if side == PositionSide.FLAT:
|
|
499
|
+
if signed_amount > 0:
|
|
500
|
+
side = PositionSide.LONG
|
|
501
|
+
elif signed_amount < 0:
|
|
502
|
+
side = PositionSide.SHORT
|
|
503
|
+
position = Position(
|
|
504
|
+
symbol=symbol,
|
|
505
|
+
exchange=self._exchange_id,
|
|
506
|
+
signed_amount=signed_amount,
|
|
507
|
+
side=side,
|
|
508
|
+
entry_price=float(position.ep),
|
|
509
|
+
unrealized_pnl=float(position.up),
|
|
510
|
+
realized_pnl=float(position.cr),
|
|
511
|
+
)
|
|
512
|
+
self._cache._apply_position(position)
|
|
513
|
+
|
|
514
|
+
def _parse_out_bound_account_position(self, raw: bytes):
|
|
515
|
+
data = self._ws_msg_spot_account_update_decoder.decode(raw)
|
|
516
|
+
res = data.event
|
|
517
|
+
self._log.debug(f"Out bound account position: {res}")
|
|
518
|
+
|
|
519
|
+
balances = res.parse_to_balances()
|
|
520
|
+
self._cache._apply_balance(account_type=self._account_type, balances=balances)
|
|
521
|
+
|
|
522
|
+
def _parse_pm_out_bound_account_position(self, raw: bytes):
|
|
523
|
+
res = self._ws_msg_pm_margin_account_update_decoder.decode(raw)
|
|
524
|
+
self._log.debug(f"Out bound account position: {res}")
|
|
525
|
+
|
|
526
|
+
balances = res.pm_parse_to_balances()
|
|
527
|
+
self._cache._apply_balance(account_type=self._account_type, balances=balances)
|
|
528
|
+
|
|
529
|
+
async def _execute_order_request(
|
|
530
|
+
self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
|
|
531
|
+
):
|
|
532
|
+
"""Execute order request based on account type and market.
|
|
533
|
+
|
|
534
|
+
Args:
|
|
535
|
+
market: BinanceMarket object
|
|
536
|
+
symbol: Trading symbol
|
|
537
|
+
params: Order parameters
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
API response
|
|
541
|
+
|
|
542
|
+
Raises:
|
|
543
|
+
ValueError: If market type is not supported for the account type
|
|
544
|
+
"""
|
|
545
|
+
if self._account_type.is_spot:
|
|
546
|
+
if not market.spot:
|
|
547
|
+
raise ValueError(
|
|
548
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
549
|
+
)
|
|
550
|
+
return await self._api_client.post_api_v3_order(**params)
|
|
551
|
+
elif self._account_type.is_isolated_margin_or_margin:
|
|
552
|
+
if not market.margin:
|
|
553
|
+
raise ValueError(
|
|
554
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
555
|
+
)
|
|
556
|
+
return await self._api_client.post_sapi_v1_margin_order(**params)
|
|
557
|
+
elif self._account_type.is_linear:
|
|
558
|
+
if not market.linear:
|
|
559
|
+
raise ValueError(
|
|
560
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
561
|
+
)
|
|
562
|
+
return await self._api_client.post_fapi_v1_order(**params)
|
|
563
|
+
elif self._account_type.is_inverse:
|
|
564
|
+
if not market.inverse:
|
|
565
|
+
raise ValueError(
|
|
566
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
567
|
+
)
|
|
568
|
+
return await self._api_client.post_dapi_v1_order(**params)
|
|
569
|
+
elif self._account_type.is_portfolio_margin:
|
|
570
|
+
if market.margin:
|
|
571
|
+
return await self._api_client.post_papi_v1_margin_order(**params)
|
|
572
|
+
elif market.linear:
|
|
573
|
+
return await self._api_client.post_papi_v1_um_order(**params)
|
|
574
|
+
elif market.inverse:
|
|
575
|
+
return await self._api_client.post_papi_v1_cm_order(**params)
|
|
576
|
+
else:
|
|
577
|
+
raise ValueError(
|
|
578
|
+
f"Unsupported market type for portfolio margin order: {symbol}"
|
|
579
|
+
)
|
|
580
|
+
else:
|
|
581
|
+
raise ValueError(
|
|
582
|
+
f"Unsupported account type for create order: {self._account_type}"
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
async def _execute_modify_order_request(
|
|
586
|
+
self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
|
|
587
|
+
):
|
|
588
|
+
if self._account_type.is_spot_or_margin:
|
|
589
|
+
raise ValueError(
|
|
590
|
+
"Modify order is not supported for `spot` or `margin` account"
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
elif self._account_type.is_linear:
|
|
594
|
+
return await self._api_client.put_fapi_v1_order(**params)
|
|
595
|
+
elif self._account_type.is_inverse:
|
|
596
|
+
return await self._api_client.put_dapi_v1_order(**params)
|
|
597
|
+
elif self._account_type.is_portfolio_margin:
|
|
598
|
+
if market.inverse:
|
|
599
|
+
return await self._api_client.put_papi_v1_cm_order(**params)
|
|
600
|
+
elif market.linear:
|
|
601
|
+
return await self._api_client.put_papi_v1_um_order(**params)
|
|
602
|
+
else:
|
|
603
|
+
raise ValueError(f"Modify order is not supported for {symbol}")
|
|
604
|
+
else:
|
|
605
|
+
raise ValueError(
|
|
606
|
+
f"Unsupported account type for modify order: {self._account_type}"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
async def _execute_cancel_order_request(
|
|
610
|
+
self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
|
|
611
|
+
):
|
|
612
|
+
if self._account_type.is_spot:
|
|
613
|
+
if not market.spot:
|
|
614
|
+
raise ValueError(
|
|
615
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
616
|
+
)
|
|
617
|
+
return await self._api_client.delete_api_v3_order(**params)
|
|
618
|
+
elif self._account_type.is_isolated_margin_or_margin:
|
|
619
|
+
if not market.margin:
|
|
620
|
+
raise ValueError(
|
|
621
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
622
|
+
)
|
|
623
|
+
return await self._api_client.delete_sapi_v1_margin_order(**params)
|
|
624
|
+
elif self._account_type.is_linear:
|
|
625
|
+
if not market.linear:
|
|
626
|
+
raise ValueError(
|
|
627
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
628
|
+
)
|
|
629
|
+
return await self._api_client.delete_fapi_v1_order(**params)
|
|
630
|
+
elif self._account_type.is_inverse:
|
|
631
|
+
if not market.inverse:
|
|
632
|
+
raise ValueError(
|
|
633
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
634
|
+
)
|
|
635
|
+
return await self._api_client.delete_dapi_v1_order(**params)
|
|
636
|
+
elif self._account_type.is_portfolio_margin:
|
|
637
|
+
if market.margin:
|
|
638
|
+
return await self._api_client.delete_papi_v1_margin_order(**params)
|
|
639
|
+
elif market.linear:
|
|
640
|
+
return await self._api_client.delete_papi_v1_um_order(**params)
|
|
641
|
+
elif market.inverse:
|
|
642
|
+
return await self._api_client.delete_papi_v1_cm_order(**params)
|
|
643
|
+
else:
|
|
644
|
+
raise ValueError(
|
|
645
|
+
f"Unsupported market type for portfolio margin cancel: {symbol}"
|
|
646
|
+
)
|
|
647
|
+
else:
|
|
648
|
+
raise ValueError(
|
|
649
|
+
f"Unsupported account type for cancel order: {self._account_type}"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
async def _execute_query_order_request(
|
|
653
|
+
self, market: BinanceMarket, symbol: str, params: Dict[str, Any]
|
|
654
|
+
):
|
|
655
|
+
if self._account_type.is_spot:
|
|
656
|
+
if not market.spot:
|
|
657
|
+
raise ValueError(
|
|
658
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
659
|
+
)
|
|
660
|
+
return await self._api_client.get_api_v3_order(**params)
|
|
661
|
+
elif self._account_type.is_isolated_margin_or_margin:
|
|
662
|
+
if not market.margin:
|
|
663
|
+
raise ValueError(
|
|
664
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
665
|
+
)
|
|
666
|
+
return await self._api_client.get_sapi_v1_margin_order(**params)
|
|
667
|
+
elif self._account_type.is_linear:
|
|
668
|
+
if not market.linear:
|
|
669
|
+
raise ValueError(
|
|
670
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
671
|
+
)
|
|
672
|
+
return await self._api_client.get_fapi_v1_order(**params)
|
|
673
|
+
elif self._account_type.is_inverse:
|
|
674
|
+
if not market.inverse:
|
|
675
|
+
raise ValueError(
|
|
676
|
+
f"BinanceAccountType.{self._account_type.value} is not supported for {symbol}"
|
|
677
|
+
)
|
|
678
|
+
return await self._api_client.get_dapi_v1_order(**params)
|
|
679
|
+
elif self._account_type.is_portfolio_margin:
|
|
680
|
+
if market.spot:
|
|
681
|
+
return await self._api_client.get_papi_v1_margin_order(**params)
|
|
682
|
+
elif market.linear:
|
|
683
|
+
return await self._api_client.get_papi_v1_um_order(**params)
|
|
684
|
+
elif market.inverse:
|
|
685
|
+
return await self._api_client.get_papi_v1_cm_order(**params)
|
|
686
|
+
else:
|
|
687
|
+
raise ValueError(
|
|
688
|
+
f"Unsupported market type for portfolio margin query: {symbol}"
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
raise ValueError(
|
|
692
|
+
f"Unsupported account type for query order: {self._account_type}"
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
def _parse_query_order(
|
|
696
|
+
self,
|
|
697
|
+
*,
|
|
698
|
+
res: BinanceOrder,
|
|
699
|
+
market: BinanceMarket,
|
|
700
|
+
symbol: str,
|
|
701
|
+
fallback_oid: str,
|
|
702
|
+
) -> Order | None:
|
|
703
|
+
if res.status is None:
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
if market.spot:
|
|
707
|
+
type = BinanceEnumParser.parse_spot_order_type(res.type)
|
|
708
|
+
else:
|
|
709
|
+
type = BinanceEnumParser.parse_futures_order_type(res.type, res.timeInForce)
|
|
710
|
+
|
|
711
|
+
amount = Decimal(res.origQty)
|
|
712
|
+
filled = Decimal(res.executedQty) if res.executedQty else Decimal(0)
|
|
713
|
+
remaining = amount - filled
|
|
714
|
+
|
|
715
|
+
return Order(
|
|
716
|
+
exchange=self._exchange_id,
|
|
717
|
+
symbol=symbol,
|
|
718
|
+
status=BinanceEnumParser.parse_order_status(res.status),
|
|
719
|
+
eid=str(res.orderId),
|
|
720
|
+
oid=res.clientOrderId or fallback_oid,
|
|
721
|
+
amount=amount,
|
|
722
|
+
filled=filled,
|
|
723
|
+
timestamp=res.updateTime or res.time or self._clock.timestamp_ms(),
|
|
724
|
+
type=type,
|
|
725
|
+
side=BinanceEnumParser.parse_order_side(res.side) if res.side else None,
|
|
726
|
+
time_in_force=BinanceEnumParser.parse_time_in_force(res.timeInForce)
|
|
727
|
+
if res.timeInForce
|
|
728
|
+
else None,
|
|
729
|
+
price=float(res.price) if res.price else None,
|
|
730
|
+
average=float(res.avgPrice) if res.avgPrice else None,
|
|
731
|
+
remaining=remaining,
|
|
732
|
+
reduce_only=res.reduceOnly,
|
|
733
|
+
position_side=BinanceEnumParser.parse_position_side(res.positionSide)
|
|
734
|
+
if res.positionSide
|
|
735
|
+
else None,
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
async def _execute_cancel_all_orders_request(
|
|
739
|
+
self, market: BinanceMarket, params: Dict[str, Any]
|
|
740
|
+
):
|
|
741
|
+
res = {}
|
|
742
|
+
if self._account_type.is_spot:
|
|
743
|
+
res = await self._api_client.delete_api_v3_open_orders(**params)
|
|
744
|
+
elif self._account_type.is_isolated_margin_or_margin:
|
|
745
|
+
res = await self._api_client.delete_sapi_v1_margin_open_orders(**params)
|
|
746
|
+
elif self._account_type.is_linear:
|
|
747
|
+
res = await self._api_client.delete_fapi_v1_all_open_orders(**params)
|
|
748
|
+
elif self._account_type.is_inverse:
|
|
749
|
+
res = await self._api_client.delete_dapi_v1_all_open_orders(**params)
|
|
750
|
+
elif self._account_type.is_portfolio_margin:
|
|
751
|
+
if market.margin:
|
|
752
|
+
res = await self._api_client.delete_papi_v1_margin_all_open_orders(
|
|
753
|
+
**params
|
|
754
|
+
)
|
|
755
|
+
elif market.linear:
|
|
756
|
+
res = await self._api_client.delete_papi_v1_um_all_open_orders(**params)
|
|
757
|
+
elif market.inverse:
|
|
758
|
+
res = await self._api_client.delete_papi_v1_cm_all_open_orders(**params)
|
|
759
|
+
|
|
760
|
+
if isinstance(res, list):
|
|
761
|
+
return # spot and margin return a list of canceled orders
|
|
762
|
+
|
|
763
|
+
if not (code := int(res.get("code", 0))) == 200:
|
|
764
|
+
msg = res.get("msg", "Unknown error")
|
|
765
|
+
raise ValueError(f"Cancel all orders failed: {code} {msg}")
|
|
766
|
+
|
|
767
|
+
async def _execute_batch_order_request(self, batch_orders: list[Dict[str, Any]]):
|
|
768
|
+
if self._account_type.is_linear:
|
|
769
|
+
return await self._api_client.post_fapi_v1_batch_orders(
|
|
770
|
+
batch_orders=batch_orders
|
|
771
|
+
)
|
|
772
|
+
elif self._account_type.is_inverse:
|
|
773
|
+
return await self._api_client.post_dapi_v1_batch_orders(
|
|
774
|
+
batch_orders=batch_orders
|
|
775
|
+
)
|
|
776
|
+
else:
|
|
777
|
+
raise ValueError(
|
|
778
|
+
f"Batch order is not supported for {self._account_type.value} account type"
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
async def _execute_order_request_ws(
|
|
782
|
+
self, oid: str, market: BinanceMarket, symbol: str, params: Dict[str, Any]
|
|
783
|
+
):
|
|
784
|
+
if self._ws_api_client is None:
|
|
785
|
+
raise ValueError(
|
|
786
|
+
f"`create_order_ws` is not supported for {self._account_type.value} account type"
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
if market.spot:
|
|
790
|
+
await self._ws_api_client.spot_new_order(oid=oid, **params)
|
|
791
|
+
elif market.linear:
|
|
792
|
+
await self._ws_api_client.usdm_new_order(oid=oid, **params)
|
|
793
|
+
else:
|
|
794
|
+
await self._ws_api_client.coinm_new_order(oid=oid, **params)
|
|
795
|
+
|
|
796
|
+
async def create_order_ws(
|
|
797
|
+
self,
|
|
798
|
+
oid: str,
|
|
799
|
+
symbol: str,
|
|
800
|
+
side: OrderSide,
|
|
801
|
+
type: OrderType,
|
|
802
|
+
amount: Decimal,
|
|
803
|
+
price: Decimal | None = None,
|
|
804
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
805
|
+
reduce_only: bool = False,
|
|
806
|
+
**kwargs,
|
|
807
|
+
) -> None:
|
|
808
|
+
self._registry.register_tmp_order(
|
|
809
|
+
order=Order(
|
|
810
|
+
oid=oid,
|
|
811
|
+
exchange=self._exchange_id,
|
|
812
|
+
symbol=symbol,
|
|
813
|
+
status=OrderStatus.INITIALIZED,
|
|
814
|
+
amount=amount,
|
|
815
|
+
type=type,
|
|
816
|
+
side=side,
|
|
817
|
+
price=float(price) if price else None,
|
|
818
|
+
time_in_force=time_in_force,
|
|
819
|
+
timestamp=self._clock.timestamp_ms(),
|
|
820
|
+
reduce_only=reduce_only,
|
|
821
|
+
)
|
|
822
|
+
)
|
|
823
|
+
market = self._market.get(symbol)
|
|
824
|
+
if not market:
|
|
825
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
826
|
+
id = market.id
|
|
827
|
+
|
|
828
|
+
params = {
|
|
829
|
+
"symbol": id,
|
|
830
|
+
"newClientOrderId": oid,
|
|
831
|
+
"side": BinanceEnumParser.to_binance_order_side(side).value,
|
|
832
|
+
"quantity": amount,
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
if type.is_post_only:
|
|
836
|
+
if market.spot:
|
|
837
|
+
params["type"] = BinanceOrderType.LIMIT_MAKER.value
|
|
838
|
+
else:
|
|
839
|
+
params["type"] = BinanceOrderType.LIMIT.value
|
|
840
|
+
params["timeInForce"] = (
|
|
841
|
+
BinanceTimeInForce.GTX.value
|
|
842
|
+
) # for future, you need to set ordertype to LIMIT and timeinforce to GTX to place a post only order
|
|
843
|
+
else:
|
|
844
|
+
params["type"] = BinanceEnumParser.to_binance_order_type(type).value
|
|
845
|
+
|
|
846
|
+
if type.is_limit or type.is_post_only:
|
|
847
|
+
if not price:
|
|
848
|
+
raise ValueError("Price is required for order")
|
|
849
|
+
params["price"] = price
|
|
850
|
+
|
|
851
|
+
if type.is_limit:
|
|
852
|
+
if time_in_force is None:
|
|
853
|
+
raise ValueError("time_in_force is required for limit orders")
|
|
854
|
+
params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
|
|
855
|
+
time_in_force
|
|
856
|
+
).value
|
|
857
|
+
|
|
858
|
+
if reduce_only:
|
|
859
|
+
params["reduceOnly"] = "true"
|
|
860
|
+
|
|
861
|
+
params.update(kwargs)
|
|
862
|
+
try:
|
|
863
|
+
await self._execute_order_request_ws(
|
|
864
|
+
oid=oid, market=market, symbol=symbol, params=params
|
|
865
|
+
)
|
|
866
|
+
except BinanceRateLimitError as e:
|
|
867
|
+
order = self._rate_limit_failed_order(
|
|
868
|
+
oid=oid,
|
|
869
|
+
symbol=symbol,
|
|
870
|
+
side=side,
|
|
871
|
+
type=type,
|
|
872
|
+
amount=amount,
|
|
873
|
+
price=price,
|
|
874
|
+
time_in_force=time_in_force,
|
|
875
|
+
reduce_only=reduce_only,
|
|
876
|
+
exc=e,
|
|
877
|
+
)
|
|
878
|
+
self.order_status_update(order)
|
|
879
|
+
|
|
880
|
+
async def create_order(
|
|
881
|
+
self,
|
|
882
|
+
oid: str,
|
|
883
|
+
symbol: str,
|
|
884
|
+
side: OrderSide,
|
|
885
|
+
type: OrderType,
|
|
886
|
+
amount: Decimal,
|
|
887
|
+
price: Decimal | None = None,
|
|
888
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
889
|
+
reduce_only: bool = False,
|
|
890
|
+
**kwargs,
|
|
891
|
+
) -> Order:
|
|
892
|
+
market = self._market.get(symbol)
|
|
893
|
+
if not market:
|
|
894
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
895
|
+
id = market.id
|
|
896
|
+
|
|
897
|
+
params = {
|
|
898
|
+
"symbol": id,
|
|
899
|
+
"newClientOrderId": oid,
|
|
900
|
+
"side": BinanceEnumParser.to_binance_order_side(side).value,
|
|
901
|
+
"quantity": str(amount),
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
if type.is_post_only:
|
|
905
|
+
if market.spot:
|
|
906
|
+
params["type"] = BinanceOrderType.LIMIT_MAKER.value
|
|
907
|
+
else:
|
|
908
|
+
params["type"] = BinanceOrderType.LIMIT.value
|
|
909
|
+
params["timeInForce"] = (
|
|
910
|
+
BinanceTimeInForce.GTX.value
|
|
911
|
+
) # for future, you need to set ordertype to LIMIT and timeinforce to GTX to place a post only order
|
|
912
|
+
else:
|
|
913
|
+
params["type"] = BinanceEnumParser.to_binance_order_type(type).value
|
|
914
|
+
|
|
915
|
+
if type.is_limit or type.is_post_only:
|
|
916
|
+
if not price:
|
|
917
|
+
raise ValueError("Price is required for order")
|
|
918
|
+
params["price"] = str(price)
|
|
919
|
+
|
|
920
|
+
if type.is_limit:
|
|
921
|
+
if time_in_force is None:
|
|
922
|
+
raise ValueError("time_in_force is required for limit orders")
|
|
923
|
+
params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
|
|
924
|
+
time_in_force
|
|
925
|
+
).value
|
|
926
|
+
|
|
927
|
+
if reduce_only:
|
|
928
|
+
params["reduceOnly"] = "true"
|
|
929
|
+
|
|
930
|
+
params.update(kwargs)
|
|
931
|
+
|
|
932
|
+
try:
|
|
933
|
+
res = await self._execute_order_request(market, symbol, params)
|
|
934
|
+
order = Order(
|
|
935
|
+
oid=oid,
|
|
936
|
+
eid=str(res.orderId),
|
|
937
|
+
exchange=self._exchange_id,
|
|
938
|
+
symbol=symbol,
|
|
939
|
+
status=OrderStatus.PENDING,
|
|
940
|
+
amount=amount,
|
|
941
|
+
filled=Decimal(0),
|
|
942
|
+
timestamp=res.updateTime,
|
|
943
|
+
type=type,
|
|
944
|
+
side=side,
|
|
945
|
+
time_in_force=time_in_force,
|
|
946
|
+
price=float(res.price) if res.price else None,
|
|
947
|
+
average=float(res.avgPrice) if res.avgPrice else None,
|
|
948
|
+
remaining=amount,
|
|
949
|
+
reduce_only=reduce_only,
|
|
950
|
+
)
|
|
951
|
+
except Exception as e:
|
|
952
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
953
|
+
self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
|
|
954
|
+
order = Order(
|
|
955
|
+
oid=oid,
|
|
956
|
+
exchange=self._exchange_id,
|
|
957
|
+
timestamp=self._clock.timestamp_ms(),
|
|
958
|
+
symbol=symbol,
|
|
959
|
+
type=type,
|
|
960
|
+
side=side,
|
|
961
|
+
amount=amount,
|
|
962
|
+
price=float(price) if price else None,
|
|
963
|
+
time_in_force=time_in_force,
|
|
964
|
+
status=OrderStatus.FAILED,
|
|
965
|
+
filled=Decimal(0),
|
|
966
|
+
remaining=amount,
|
|
967
|
+
reduce_only=reduce_only,
|
|
968
|
+
reason=error_msg,
|
|
969
|
+
)
|
|
970
|
+
self.order_status_update(order)
|
|
971
|
+
return order
|
|
972
|
+
|
|
973
|
+
async def _execute_cancel_order_request_ws(
|
|
974
|
+
self, oid: str, market: BinanceMarket, params: Dict[str, Any]
|
|
975
|
+
):
|
|
976
|
+
if self._ws_api_client is None:
|
|
977
|
+
raise ValueError(
|
|
978
|
+
f"`create_order_ws` is not supported for {self._account_type.value} account type"
|
|
979
|
+
)
|
|
980
|
+
|
|
981
|
+
if market.spot:
|
|
982
|
+
await self._ws_api_client.spot_cancel_order(oid=oid, **params)
|
|
983
|
+
elif market.linear:
|
|
984
|
+
await self._ws_api_client.usdm_cancel_order(oid=oid, **params)
|
|
985
|
+
else:
|
|
986
|
+
await self._ws_api_client.coinm_cancel_order(oid=oid, **params)
|
|
987
|
+
|
|
988
|
+
async def cancel_order_ws(self, oid: str, symbol: str, **kwargs) -> None:
|
|
989
|
+
market = self._market.get(symbol)
|
|
990
|
+
if not market:
|
|
991
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
992
|
+
id = market.id
|
|
993
|
+
params = {
|
|
994
|
+
"symbol": id,
|
|
995
|
+
"origClientOrderId": oid,
|
|
996
|
+
**kwargs,
|
|
997
|
+
}
|
|
998
|
+
await self._execute_cancel_order_request_ws(oid, market, params)
|
|
999
|
+
|
|
1000
|
+
async def cancel_order(self, oid: str, symbol: str, **kwargs) -> Order:
|
|
1001
|
+
try:
|
|
1002
|
+
market = self._market.get(symbol)
|
|
1003
|
+
if not market:
|
|
1004
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1005
|
+
id = market.id
|
|
1006
|
+
|
|
1007
|
+
params = {
|
|
1008
|
+
"symbol": id,
|
|
1009
|
+
"origClientOrderId": oid,
|
|
1010
|
+
**kwargs,
|
|
1011
|
+
}
|
|
1012
|
+
if not market.linear or not market.inverse:
|
|
1013
|
+
params["newClientOrderId"] = oid
|
|
1014
|
+
|
|
1015
|
+
res = await self._execute_cancel_order_request(market, symbol, params)
|
|
1016
|
+
|
|
1017
|
+
if market.spot:
|
|
1018
|
+
type = (
|
|
1019
|
+
BinanceEnumParser.parse_spot_order_type(res.type)
|
|
1020
|
+
if res.type
|
|
1021
|
+
else None
|
|
1022
|
+
)
|
|
1023
|
+
else:
|
|
1024
|
+
type = (
|
|
1025
|
+
BinanceEnumParser.parse_futures_order_type(
|
|
1026
|
+
res.type, res.timeInForce
|
|
1027
|
+
)
|
|
1028
|
+
if res.type
|
|
1029
|
+
else None
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
order = Order(
|
|
1033
|
+
exchange=self._exchange_id,
|
|
1034
|
+
symbol=symbol,
|
|
1035
|
+
status=OrderStatus.CANCELING,
|
|
1036
|
+
eid=str(res.orderId),
|
|
1037
|
+
oid=res.clientOrderId,
|
|
1038
|
+
amount=Decimal(res.origQty) if res.origQty else None,
|
|
1039
|
+
filled=Decimal(res.executedQty) if res.executedQty else Decimal(0),
|
|
1040
|
+
timestamp=res.updateTime,
|
|
1041
|
+
type=type,
|
|
1042
|
+
side=BinanceEnumParser.parse_order_side(res.side) if res.side else None,
|
|
1043
|
+
time_in_force=BinanceEnumParser.parse_time_in_force(res.timeInForce)
|
|
1044
|
+
if res.timeInForce
|
|
1045
|
+
else None,
|
|
1046
|
+
price=float(res.price) if res.price else None,
|
|
1047
|
+
average=float(res.avgPrice) if res.avgPrice else None,
|
|
1048
|
+
remaining=(Decimal(res.origQty) - Decimal(res.executedQty))
|
|
1049
|
+
if res.origQty and res.executedQty
|
|
1050
|
+
else None,
|
|
1051
|
+
reduce_only=res.reduceOnly,
|
|
1052
|
+
position_side=BinanceEnumParser.parse_position_side(res.positionSide)
|
|
1053
|
+
if res.positionSide
|
|
1054
|
+
else None,
|
|
1055
|
+
)
|
|
1056
|
+
except BinanceRateLimitError as e:
|
|
1057
|
+
error_msg = f"rate_limit (retry_after={e.retry_after:.1f}s, type={e.rate_limit_type}): {str(e)}"
|
|
1058
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
1059
|
+
order = Order(
|
|
1060
|
+
exchange=self._exchange_id,
|
|
1061
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1062
|
+
symbol=symbol,
|
|
1063
|
+
oid=oid,
|
|
1064
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1065
|
+
reason=error_msg,
|
|
1066
|
+
)
|
|
1067
|
+
except (BinanceClientError, BinanceServerError) as e:
|
|
1068
|
+
error_msg = str(e)
|
|
1069
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
1070
|
+
order = Order(
|
|
1071
|
+
exchange=self._exchange_id,
|
|
1072
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1073
|
+
symbol=symbol,
|
|
1074
|
+
oid=oid,
|
|
1075
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1076
|
+
reason=error_msg,
|
|
1077
|
+
)
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1080
|
+
self._log.error(f"Error canceling order: {error_msg} params: {str(params)}")
|
|
1081
|
+
order = Order(
|
|
1082
|
+
exchange=self._exchange_id,
|
|
1083
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1084
|
+
symbol=symbol,
|
|
1085
|
+
oid=oid,
|
|
1086
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1087
|
+
reason=error_msg,
|
|
1088
|
+
)
|
|
1089
|
+
self.order_status_update(order)
|
|
1090
|
+
return order
|
|
1091
|
+
|
|
1092
|
+
async def query_order(self, oid: str, symbol: str) -> Order | None:
|
|
1093
|
+
market = self._market.get(symbol)
|
|
1094
|
+
if not market:
|
|
1095
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1096
|
+
|
|
1097
|
+
params = {
|
|
1098
|
+
"symbol": market.id,
|
|
1099
|
+
"origClientOrderId": oid,
|
|
1100
|
+
}
|
|
1101
|
+
try:
|
|
1102
|
+
res = await self._execute_query_order_request(market, symbol, params)
|
|
1103
|
+
return self._parse_query_order(
|
|
1104
|
+
res=res,
|
|
1105
|
+
market=market,
|
|
1106
|
+
symbol=symbol,
|
|
1107
|
+
fallback_oid=oid,
|
|
1108
|
+
)
|
|
1109
|
+
except BinanceClientError as e:
|
|
1110
|
+
if e.code in {-2013, -2026}:
|
|
1111
|
+
return Order(
|
|
1112
|
+
exchange=self._exchange_id,
|
|
1113
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1114
|
+
symbol=symbol,
|
|
1115
|
+
oid=oid,
|
|
1116
|
+
status=OrderStatus.CANCELED,
|
|
1117
|
+
reason=str(e),
|
|
1118
|
+
)
|
|
1119
|
+
error_msg = str(e)
|
|
1120
|
+
self._log.error(f"Error querying order: {error_msg} params: {params}")
|
|
1121
|
+
return None
|
|
1122
|
+
except (BinanceRateLimitError, BinanceServerError) as e:
|
|
1123
|
+
error_msg = str(e)
|
|
1124
|
+
self._log.error(f"Error querying order: {error_msg} params: {params}")
|
|
1125
|
+
return None
|
|
1126
|
+
except Exception as e:
|
|
1127
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1128
|
+
self._log.error(f"Error querying order: {error_msg} params: {params}")
|
|
1129
|
+
return None
|
|
1130
|
+
|
|
1131
|
+
async def cancel_all_orders(self, symbol: str) -> bool:
|
|
1132
|
+
try:
|
|
1133
|
+
market = self._market.get(symbol)
|
|
1134
|
+
if not market:
|
|
1135
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1136
|
+
symbol = market.id
|
|
1137
|
+
|
|
1138
|
+
params = {
|
|
1139
|
+
"symbol": symbol,
|
|
1140
|
+
}
|
|
1141
|
+
await self._execute_cancel_all_orders_request(market, params)
|
|
1142
|
+
return True
|
|
1143
|
+
except Exception as e:
|
|
1144
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1145
|
+
self._log.error(
|
|
1146
|
+
f"Error canceling all orders: {error_msg} params: {str(params)}"
|
|
1147
|
+
)
|
|
1148
|
+
return False
|
|
1149
|
+
|
|
1150
|
+
async def modify_order(
|
|
1151
|
+
self,
|
|
1152
|
+
oid: str,
|
|
1153
|
+
symbol: str,
|
|
1154
|
+
side: OrderSide | None = None,
|
|
1155
|
+
price: Decimal | None = None,
|
|
1156
|
+
amount: Decimal | None = None,
|
|
1157
|
+
**kwargs,
|
|
1158
|
+
) -> Order:
|
|
1159
|
+
market = self._market.get(symbol)
|
|
1160
|
+
if not market:
|
|
1161
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1162
|
+
id = market.id
|
|
1163
|
+
|
|
1164
|
+
if market.spot:
|
|
1165
|
+
raise ValueError(
|
|
1166
|
+
"Modify order is not supported for `spot` account type, please cancel and create a new order"
|
|
1167
|
+
)
|
|
1168
|
+
if side is None:
|
|
1169
|
+
raise ValueError("side is required to modify a Binance order")
|
|
1170
|
+
|
|
1171
|
+
params = {
|
|
1172
|
+
"symbol": id,
|
|
1173
|
+
"origClientOrderId": oid,
|
|
1174
|
+
"side": BinanceEnumParser.to_binance_order_side(side).value,
|
|
1175
|
+
"quantity": str(amount) if amount else None,
|
|
1176
|
+
"price": str(price) if price else None,
|
|
1177
|
+
**kwargs,
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
try:
|
|
1181
|
+
res = await self._execute_modify_order_request(market, symbol, params)
|
|
1182
|
+
order = Order(
|
|
1183
|
+
exchange=self._exchange_id,
|
|
1184
|
+
symbol=symbol,
|
|
1185
|
+
status=OrderStatus.PENDING,
|
|
1186
|
+
eid=str(res.orderId),
|
|
1187
|
+
oid=oid,
|
|
1188
|
+
amount=amount,
|
|
1189
|
+
filled=Decimal(res.executedQty),
|
|
1190
|
+
timestamp=res.updateTime,
|
|
1191
|
+
type=BinanceEnumParser.parse_futures_order_type(
|
|
1192
|
+
res.type, res.timeInForce
|
|
1193
|
+
),
|
|
1194
|
+
side=side,
|
|
1195
|
+
time_in_force=BinanceEnumParser.parse_time_in_force(res.timeInForce),
|
|
1196
|
+
price=float(res.price) if res.price else None,
|
|
1197
|
+
average=float(res.avgPrice) if res.avgPrice else None,
|
|
1198
|
+
remaining=Decimal(res.origQty) - Decimal(res.executedQty),
|
|
1199
|
+
reduce_only=res.reduceOnly,
|
|
1200
|
+
position_side=BinanceEnumParser.parse_position_side(res.positionSide)
|
|
1201
|
+
if res.positionSide
|
|
1202
|
+
else None,
|
|
1203
|
+
)
|
|
1204
|
+
except Exception as e:
|
|
1205
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1206
|
+
self._log.error(f"Error modifying order: {error_msg} params: {str(params)}")
|
|
1207
|
+
order = Order(
|
|
1208
|
+
exchange=self._exchange_id,
|
|
1209
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1210
|
+
oid=oid,
|
|
1211
|
+
symbol=symbol,
|
|
1212
|
+
side=side,
|
|
1213
|
+
amount=amount,
|
|
1214
|
+
price=float(price) if price else None,
|
|
1215
|
+
status=OrderStatus.FAILED,
|
|
1216
|
+
filled=Decimal("0"),
|
|
1217
|
+
remaining=amount,
|
|
1218
|
+
reason=error_msg,
|
|
1219
|
+
)
|
|
1220
|
+
self.order_status_update(order)
|
|
1221
|
+
return order
|
|
1222
|
+
|
|
1223
|
+
async def create_tp_sl_order(
|
|
1224
|
+
self,
|
|
1225
|
+
oid: str,
|
|
1226
|
+
symbol: str,
|
|
1227
|
+
side: OrderSide,
|
|
1228
|
+
type: OrderType,
|
|
1229
|
+
amount: Decimal,
|
|
1230
|
+
price: Decimal | None = None,
|
|
1231
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC,
|
|
1232
|
+
tp_order_type: OrderType | None = None,
|
|
1233
|
+
tp_trigger_price: Decimal | None = None,
|
|
1234
|
+
tp_price: Decimal | None = None,
|
|
1235
|
+
tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
|
|
1236
|
+
sl_order_type: OrderType | None = None,
|
|
1237
|
+
sl_trigger_price: Decimal | None = None,
|
|
1238
|
+
sl_price: Decimal | None = None,
|
|
1239
|
+
sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
|
|
1240
|
+
**kwargs,
|
|
1241
|
+
) -> Order:
|
|
1242
|
+
tasks = []
|
|
1243
|
+
tasks.append(
|
|
1244
|
+
self.create_order(
|
|
1245
|
+
oid=oid,
|
|
1246
|
+
symbol=symbol,
|
|
1247
|
+
side=side,
|
|
1248
|
+
type=type,
|
|
1249
|
+
amount=amount,
|
|
1250
|
+
price=price,
|
|
1251
|
+
time_in_force=time_in_force,
|
|
1252
|
+
**kwargs,
|
|
1253
|
+
)
|
|
1254
|
+
)
|
|
1255
|
+
tp_sl_side = OrderSide.SELL if side.is_buy else OrderSide.BUY
|
|
1256
|
+
effective_tp_trigger_type = tp_trigger_type or TriggerType.LAST_PRICE
|
|
1257
|
+
effective_sl_trigger_type = sl_trigger_type or TriggerType.LAST_PRICE
|
|
1258
|
+
if tp_order_type and tp_trigger_price:
|
|
1259
|
+
tasks.append(
|
|
1260
|
+
self._create_take_profit_order(
|
|
1261
|
+
symbol=symbol,
|
|
1262
|
+
side=tp_sl_side,
|
|
1263
|
+
type=tp_order_type,
|
|
1264
|
+
amount=amount,
|
|
1265
|
+
trigger_price=tp_trigger_price,
|
|
1266
|
+
price=tp_price,
|
|
1267
|
+
trigger_type=effective_tp_trigger_type,
|
|
1268
|
+
)
|
|
1269
|
+
)
|
|
1270
|
+
if sl_order_type and sl_trigger_price:
|
|
1271
|
+
tasks.append(
|
|
1272
|
+
self._create_stop_loss_order(
|
|
1273
|
+
symbol=symbol,
|
|
1274
|
+
side=tp_sl_side,
|
|
1275
|
+
type=sl_order_type,
|
|
1276
|
+
amount=amount,
|
|
1277
|
+
trigger_price=sl_trigger_price,
|
|
1278
|
+
price=sl_price,
|
|
1279
|
+
trigger_type=effective_sl_trigger_type,
|
|
1280
|
+
)
|
|
1281
|
+
)
|
|
1282
|
+
results = await asyncio.gather(*tasks)
|
|
1283
|
+
return results[0]
|
|
1284
|
+
# return res[0]
|
|
1285
|
+
|
|
1286
|
+
async def _create_stop_loss_order(
|
|
1287
|
+
self,
|
|
1288
|
+
symbol: str,
|
|
1289
|
+
side: OrderSide,
|
|
1290
|
+
type: OrderType,
|
|
1291
|
+
amount: Decimal,
|
|
1292
|
+
trigger_price: Decimal,
|
|
1293
|
+
trigger_type: TriggerType = TriggerType.LAST_PRICE,
|
|
1294
|
+
price: Decimal | None = None,
|
|
1295
|
+
time_in_force: TimeInForce = TimeInForce.GTC,
|
|
1296
|
+
position_side: PositionSide | None = None,
|
|
1297
|
+
**kwargs,
|
|
1298
|
+
):
|
|
1299
|
+
market = self._market.get(symbol)
|
|
1300
|
+
if not market:
|
|
1301
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1302
|
+
|
|
1303
|
+
id = market.id
|
|
1304
|
+
|
|
1305
|
+
if market.inverse or market.linear:
|
|
1306
|
+
binance_type = (
|
|
1307
|
+
BinanceOrderType.STOP if type.is_limit else BinanceOrderType.STOP_MARKET
|
|
1308
|
+
)
|
|
1309
|
+
elif market.spot:
|
|
1310
|
+
binance_type = (
|
|
1311
|
+
BinanceOrderType.STOP_LOSS_LIMIT
|
|
1312
|
+
if type.is_limit
|
|
1313
|
+
else BinanceOrderType.STOP_LOSS
|
|
1314
|
+
)
|
|
1315
|
+
elif market.margin:
|
|
1316
|
+
# TODO: margin order is not supported yet
|
|
1317
|
+
pass
|
|
1318
|
+
|
|
1319
|
+
params = {
|
|
1320
|
+
"symbol": id,
|
|
1321
|
+
"side": BinanceEnumParser.to_binance_order_side(side).value,
|
|
1322
|
+
"type": binance_type.value,
|
|
1323
|
+
"quantity": str(amount),
|
|
1324
|
+
"stopPrice": trigger_price,
|
|
1325
|
+
"workingType": BinanceEnumParser.to_binance_trigger_type(
|
|
1326
|
+
trigger_type
|
|
1327
|
+
).value,
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if type.is_limit:
|
|
1331
|
+
if price is None:
|
|
1332
|
+
raise ValueError("Price must be provided for limit stop loss orders")
|
|
1333
|
+
|
|
1334
|
+
params["price"] = str(price)
|
|
1335
|
+
params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
|
|
1336
|
+
time_in_force
|
|
1337
|
+
).value
|
|
1338
|
+
|
|
1339
|
+
if position_side:
|
|
1340
|
+
params["positionSide"] = BinanceEnumParser.to_binance_position_side(
|
|
1341
|
+
position_side
|
|
1342
|
+
).value
|
|
1343
|
+
|
|
1344
|
+
params.update(kwargs)
|
|
1345
|
+
|
|
1346
|
+
try:
|
|
1347
|
+
await self._execute_order_request(market, symbol, params)
|
|
1348
|
+
# order = Order(
|
|
1349
|
+
# exchange=self._exchange_id,
|
|
1350
|
+
# symbol=symbol,
|
|
1351
|
+
# status=OrderStatus.PENDING,
|
|
1352
|
+
#
|
|
1353
|
+
# id=str(res.orderId),
|
|
1354
|
+
# uuid=uuid,
|
|
1355
|
+
# amount=amount,
|
|
1356
|
+
# filled=Decimal(0),
|
|
1357
|
+
# client_order_id=res.clientOrderId,
|
|
1358
|
+
# timestamp=res.updateTime,
|
|
1359
|
+
# type=type,
|
|
1360
|
+
# side=side,
|
|
1361
|
+
# time_in_force=time_in_force,
|
|
1362
|
+
# price=float(res.price) if res.price else None,
|
|
1363
|
+
# average=float(res.avgPrice) if res.avgPrice else None,
|
|
1364
|
+
# trigger_price=float(res.stopPrice),
|
|
1365
|
+
# remaining=amount,
|
|
1366
|
+
# reduce_only=res.reduceOnly if res.reduceOnly else None,
|
|
1367
|
+
# position_side=BinanceEnumParser.parse_position_side(res.positionSide)
|
|
1368
|
+
# if res.positionSide
|
|
1369
|
+
# else None,
|
|
1370
|
+
# )
|
|
1371
|
+
# return order
|
|
1372
|
+
except Exception as e:
|
|
1373
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1374
|
+
self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
|
|
1375
|
+
# order = Order(
|
|
1376
|
+
# exchange=self._exchange_id,
|
|
1377
|
+
# timestamp=self._clock.timestamp_ms(),
|
|
1378
|
+
# symbol=symbol,
|
|
1379
|
+
# uuid=uuid,
|
|
1380
|
+
# type=type,
|
|
1381
|
+
# side=side,
|
|
1382
|
+
# amount=amount,
|
|
1383
|
+
# trigger_price=trigger_price,
|
|
1384
|
+
# price=float(price) if price else None,
|
|
1385
|
+
# time_in_force=time_in_force,
|
|
1386
|
+
# position_side=position_side,
|
|
1387
|
+
# status=OrderStatus.FAILED,
|
|
1388
|
+
#
|
|
1389
|
+
# filled=Decimal(0),
|
|
1390
|
+
# remaining=amount,
|
|
1391
|
+
# )
|
|
1392
|
+
# return order
|
|
1393
|
+
|
|
1394
|
+
async def _create_take_profit_order(
|
|
1395
|
+
self,
|
|
1396
|
+
symbol: str,
|
|
1397
|
+
side: OrderSide,
|
|
1398
|
+
type: OrderType,
|
|
1399
|
+
amount: Decimal,
|
|
1400
|
+
trigger_price: Decimal,
|
|
1401
|
+
trigger_type: TriggerType = TriggerType.LAST_PRICE,
|
|
1402
|
+
price: Decimal | None = None,
|
|
1403
|
+
time_in_force: TimeInForce = TimeInForce.GTC,
|
|
1404
|
+
position_side: PositionSide | None = None,
|
|
1405
|
+
**kwargs,
|
|
1406
|
+
):
|
|
1407
|
+
market = self._market.get(symbol)
|
|
1408
|
+
if not market:
|
|
1409
|
+
raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
|
|
1410
|
+
|
|
1411
|
+
id = market.id
|
|
1412
|
+
|
|
1413
|
+
if market.inverse or market.linear:
|
|
1414
|
+
binance_type = (
|
|
1415
|
+
BinanceOrderType.TAKE_PROFIT
|
|
1416
|
+
if type.is_limit
|
|
1417
|
+
else BinanceOrderType.TAKE_PROFIT_MARKET
|
|
1418
|
+
)
|
|
1419
|
+
elif market.spot:
|
|
1420
|
+
binance_type = (
|
|
1421
|
+
BinanceOrderType.TAKE_PROFIT_LIMIT
|
|
1422
|
+
if type.is_limit
|
|
1423
|
+
else BinanceOrderType.TAKE_PROFIT
|
|
1424
|
+
)
|
|
1425
|
+
elif market.margin:
|
|
1426
|
+
# TODO: margin order is not supported yet
|
|
1427
|
+
pass
|
|
1428
|
+
|
|
1429
|
+
params = {
|
|
1430
|
+
"symbol": id,
|
|
1431
|
+
"side": BinanceEnumParser.to_binance_order_side(side).value,
|
|
1432
|
+
"type": binance_type.value,
|
|
1433
|
+
"quantity": str(amount),
|
|
1434
|
+
"stopPrice": trigger_price,
|
|
1435
|
+
"workingType": BinanceEnumParser.to_binance_trigger_type(
|
|
1436
|
+
trigger_type
|
|
1437
|
+
).value,
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
if type.is_limit:
|
|
1441
|
+
if price is None:
|
|
1442
|
+
raise ValueError("Price must be provided for limit take profit orders")
|
|
1443
|
+
|
|
1444
|
+
params["price"] = str(price)
|
|
1445
|
+
params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
|
|
1446
|
+
time_in_force
|
|
1447
|
+
).value
|
|
1448
|
+
|
|
1449
|
+
if position_side:
|
|
1450
|
+
params["positionSide"] = BinanceEnumParser.to_binance_position_side(
|
|
1451
|
+
position_side
|
|
1452
|
+
).value
|
|
1453
|
+
|
|
1454
|
+
params.update(kwargs)
|
|
1455
|
+
|
|
1456
|
+
try:
|
|
1457
|
+
await self._execute_order_request(market, symbol, params)
|
|
1458
|
+
# order = Order(
|
|
1459
|
+
# exchange=self._exchange_id,
|
|
1460
|
+
# symbol=symbol,
|
|
1461
|
+
# status=OrderStatus.PENDING,
|
|
1462
|
+
#
|
|
1463
|
+
# eid=str(res.orderId),
|
|
1464
|
+
# amount=amount,
|
|
1465
|
+
# filled=Decimal(0),
|
|
1466
|
+
# client_order_id=res.clientOrderId,
|
|
1467
|
+
# timestamp=res.updateTime,
|
|
1468
|
+
# type=type,
|
|
1469
|
+
# side=side,
|
|
1470
|
+
# time_in_force=time_in_force,
|
|
1471
|
+
# price=float(res.price) if res.price else None,
|
|
1472
|
+
# average=float(res.avgPrice) if res.avgPrice else None,
|
|
1473
|
+
# trigger_price=float(res.stopPrice),
|
|
1474
|
+
# remaining=amount,
|
|
1475
|
+
# reduce_only=res.reduceOnly if res.reduceOnly else None,
|
|
1476
|
+
# position_side=BinanceEnumParser.parse_position_side(res.positionSide)
|
|
1477
|
+
# if res.positionSide
|
|
1478
|
+
# else None,
|
|
1479
|
+
# )
|
|
1480
|
+
# return order
|
|
1481
|
+
except Exception as e:
|
|
1482
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1483
|
+
self._log.error(f"Error creating order: {error_msg} params: {str(params)}")
|
|
1484
|
+
# order = Order(
|
|
1485
|
+
# exchange=self._exchange_id,
|
|
1486
|
+
# timestamp=self._clock.timestamp_ms(),
|
|
1487
|
+
# symbol=symbol,
|
|
1488
|
+
# type=type,
|
|
1489
|
+
# side=side,
|
|
1490
|
+
# amount=amount,
|
|
1491
|
+
# trigger_price=trigger_price,
|
|
1492
|
+
# price=float(price) if price else None,
|
|
1493
|
+
# time_in_force=time_in_force,
|
|
1494
|
+
# position_side=position_side,
|
|
1495
|
+
# status=OrderStatus.FAILED,
|
|
1496
|
+
#
|
|
1497
|
+
# filled=Decimal(0),
|
|
1498
|
+
# remaining=amount,
|
|
1499
|
+
# )
|
|
1500
|
+
# return order
|
|
1501
|
+
|
|
1502
|
+
async def create_batch_orders(self, orders: list[BatchOrderSubmit]):
|
|
1503
|
+
if self._account_type.is_portfolio_margin:
|
|
1504
|
+
tasks = [
|
|
1505
|
+
self.create_order(
|
|
1506
|
+
oid=order.oid,
|
|
1507
|
+
symbol=order.symbol,
|
|
1508
|
+
side=order.side,
|
|
1509
|
+
amount=order.amount,
|
|
1510
|
+
price=order.price,
|
|
1511
|
+
type=order.type,
|
|
1512
|
+
time_in_force=order.time_in_force,
|
|
1513
|
+
reduce_only=order.reduce_only,
|
|
1514
|
+
**order.kwargs,
|
|
1515
|
+
)
|
|
1516
|
+
for order in orders
|
|
1517
|
+
]
|
|
1518
|
+
await asyncio.gather(*tasks)
|
|
1519
|
+
else:
|
|
1520
|
+
batch_orders = []
|
|
1521
|
+
for order in orders:
|
|
1522
|
+
market = self._market.get(order.symbol)
|
|
1523
|
+
if not market:
|
|
1524
|
+
raise ValueError(
|
|
1525
|
+
f"Symbol {order.symbol} formated wrongly, or not supported"
|
|
1526
|
+
)
|
|
1527
|
+
id = market.id
|
|
1528
|
+
|
|
1529
|
+
params = {
|
|
1530
|
+
"symbol": id,
|
|
1531
|
+
"newClientOrderId": order.oid,
|
|
1532
|
+
"side": BinanceEnumParser.to_binance_order_side(order.side).value,
|
|
1533
|
+
"quantity": str(order.amount),
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if order.type.is_post_only:
|
|
1537
|
+
if market.spot:
|
|
1538
|
+
params["type"] = BinanceOrderType.LIMIT_MAKER.value
|
|
1539
|
+
else:
|
|
1540
|
+
params["type"] = BinanceOrderType.LIMIT.value
|
|
1541
|
+
params["timeInForce"] = BinanceTimeInForce.GTX.value
|
|
1542
|
+
else:
|
|
1543
|
+
params["type"] = BinanceEnumParser.to_binance_order_type(
|
|
1544
|
+
order.type
|
|
1545
|
+
).value
|
|
1546
|
+
|
|
1547
|
+
if order.type.is_limit or order.type.is_post_only:
|
|
1548
|
+
if not order.price:
|
|
1549
|
+
raise ValueError("Price is required for limit order")
|
|
1550
|
+
|
|
1551
|
+
params["price"] = str(order.price)
|
|
1552
|
+
|
|
1553
|
+
if order.type.is_limit:
|
|
1554
|
+
params["timeInForce"] = BinanceEnumParser.to_binance_time_in_force(
|
|
1555
|
+
order.time_in_force
|
|
1556
|
+
).value
|
|
1557
|
+
|
|
1558
|
+
if order.reduce_only:
|
|
1559
|
+
params["reduceOnly"] = "true"
|
|
1560
|
+
|
|
1561
|
+
params.update(order.kwargs)
|
|
1562
|
+
batch_orders.append(params)
|
|
1563
|
+
try:
|
|
1564
|
+
res = await self._execute_batch_order_request(batch_orders)
|
|
1565
|
+
for order, res_order in zip(orders, res):
|
|
1566
|
+
if not res_order.code:
|
|
1567
|
+
res_batch_order = Order(
|
|
1568
|
+
exchange=self._exchange_id,
|
|
1569
|
+
symbol=order.symbol,
|
|
1570
|
+
status=OrderStatus.PENDING,
|
|
1571
|
+
eid=str(res_order.orderId),
|
|
1572
|
+
oid=order.oid,
|
|
1573
|
+
amount=order.amount,
|
|
1574
|
+
filled=Decimal(0),
|
|
1575
|
+
timestamp=res_order.updateTime,
|
|
1576
|
+
type=order.type,
|
|
1577
|
+
side=order.side,
|
|
1578
|
+
time_in_force=order.time_in_force,
|
|
1579
|
+
price=float(order.price) if order.price else None,
|
|
1580
|
+
average=float(res_order.avgPrice)
|
|
1581
|
+
if res_order.avgPrice
|
|
1582
|
+
else None,
|
|
1583
|
+
remaining=order.amount,
|
|
1584
|
+
reduce_only=order.reduce_only,
|
|
1585
|
+
position_side=BinanceEnumParser.parse_position_side(
|
|
1586
|
+
res_order.positionSide
|
|
1587
|
+
)
|
|
1588
|
+
if res_order.positionSide
|
|
1589
|
+
else None,
|
|
1590
|
+
)
|
|
1591
|
+
else:
|
|
1592
|
+
res_batch_order = Order(
|
|
1593
|
+
exchange=self._exchange_id,
|
|
1594
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1595
|
+
oid=order.oid,
|
|
1596
|
+
symbol=order.symbol,
|
|
1597
|
+
type=order.type,
|
|
1598
|
+
side=order.side,
|
|
1599
|
+
amount=order.amount,
|
|
1600
|
+
price=float(order.price) if order.price else None,
|
|
1601
|
+
time_in_force=order.time_in_force,
|
|
1602
|
+
status=OrderStatus.FAILED,
|
|
1603
|
+
filled=Decimal(0),
|
|
1604
|
+
reduce_only=order.reduce_only,
|
|
1605
|
+
remaining=order.amount,
|
|
1606
|
+
reason=res_order.msg,
|
|
1607
|
+
)
|
|
1608
|
+
self._log.error(
|
|
1609
|
+
f"Failed to place order for {order.symbol}: {res_order.msg}: oid: {order.oid}"
|
|
1610
|
+
)
|
|
1611
|
+
self.order_status_update(res_batch_order)
|
|
1612
|
+
except Exception as e:
|
|
1613
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1614
|
+
self._log.error(f"Error placing batch orders: {error_msg}")
|
|
1615
|
+
for order in orders:
|
|
1616
|
+
res_batch_order = Order(
|
|
1617
|
+
exchange=self._exchange_id,
|
|
1618
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1619
|
+
oid=order.oid,
|
|
1620
|
+
symbol=order.symbol,
|
|
1621
|
+
type=order.type,
|
|
1622
|
+
side=order.side,
|
|
1623
|
+
amount=order.amount,
|
|
1624
|
+
price=float(order.price) if order.price else None,
|
|
1625
|
+
time_in_force=order.time_in_force,
|
|
1626
|
+
status=OrderStatus.FAILED,
|
|
1627
|
+
filled=Decimal(0),
|
|
1628
|
+
remaining=order.amount,
|
|
1629
|
+
reason=error_msg,
|
|
1630
|
+
)
|
|
1631
|
+
self.order_status_update(res_batch_order)
|
|
1632
|
+
|
|
1633
|
+
async def _execute_cancel_batch_order_request(
|
|
1634
|
+
self, symbol_id: str, oids: list[str]
|
|
1635
|
+
):
|
|
1636
|
+
if self._account_type.is_linear:
|
|
1637
|
+
return await self._api_client.delete_fapi_v1_batch_orders(symbol_id, oids)
|
|
1638
|
+
elif self._account_type.is_inverse:
|
|
1639
|
+
return await self._api_client.delete_dapi_v1_batch_orders(symbol_id, oids)
|
|
1640
|
+
else:
|
|
1641
|
+
raise ValueError(
|
|
1642
|
+
f"Batch cancel not supported for {self._account_type.value}"
|
|
1643
|
+
)
|
|
1644
|
+
|
|
1645
|
+
async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]):
|
|
1646
|
+
from collections import defaultdict
|
|
1647
|
+
|
|
1648
|
+
groups = defaultdict(list)
|
|
1649
|
+
for order in orders:
|
|
1650
|
+
market = self._market.get(order.symbol)
|
|
1651
|
+
if not market:
|
|
1652
|
+
raise ValueError(f"Symbol {order.symbol} not found")
|
|
1653
|
+
groups[market.id].append(order)
|
|
1654
|
+
|
|
1655
|
+
for symbol_id, symbol_orders in groups.items():
|
|
1656
|
+
for i in range(0, len(symbol_orders), 10):
|
|
1657
|
+
chunk = symbol_orders[i : i + 10]
|
|
1658
|
+
oids = [o.oid for o in chunk]
|
|
1659
|
+
try:
|
|
1660
|
+
results = await self._execute_cancel_batch_order_request(
|
|
1661
|
+
symbol_id, oids
|
|
1662
|
+
)
|
|
1663
|
+
for res_order, submit in zip(results, chunk):
|
|
1664
|
+
if not res_order.code:
|
|
1665
|
+
order = Order(
|
|
1666
|
+
exchange=self._exchange_id,
|
|
1667
|
+
symbol=submit.symbol,
|
|
1668
|
+
oid=res_order.clientOrderId,
|
|
1669
|
+
eid=str(res_order.orderId),
|
|
1670
|
+
timestamp=res_order.updateTime,
|
|
1671
|
+
status=OrderStatus.CANCELING,
|
|
1672
|
+
)
|
|
1673
|
+
else:
|
|
1674
|
+
order = Order(
|
|
1675
|
+
exchange=self._exchange_id,
|
|
1676
|
+
symbol=submit.symbol,
|
|
1677
|
+
oid=submit.oid,
|
|
1678
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1679
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1680
|
+
reason=res_order.msg,
|
|
1681
|
+
)
|
|
1682
|
+
self._log.error(
|
|
1683
|
+
f"Batch cancel failed for {submit.symbol} oid={submit.oid}: {res_order.msg}"
|
|
1684
|
+
)
|
|
1685
|
+
self.order_status_update(order)
|
|
1686
|
+
except Exception as e:
|
|
1687
|
+
error_msg = f"{e.__class__.__name__}: {str(e)}"
|
|
1688
|
+
self._log.error(f"Error canceling batch orders: {error_msg}")
|
|
1689
|
+
for submit in chunk:
|
|
1690
|
+
order = Order(
|
|
1691
|
+
exchange=self._exchange_id,
|
|
1692
|
+
symbol=submit.symbol,
|
|
1693
|
+
oid=submit.oid,
|
|
1694
|
+
timestamp=self._clock.timestamp_ms(),
|
|
1695
|
+
status=OrderStatus.CANCEL_FAILED,
|
|
1696
|
+
reason=error_msg,
|
|
1697
|
+
)
|
|
1698
|
+
self.order_status_update(order)
|
|
1699
|
+
|
|
1700
|
+
def _apply_position(
|
|
1701
|
+
self,
|
|
1702
|
+
pos: BinanceFuturesPositionInfo | BinancePortfolioMarginPositionRisk,
|
|
1703
|
+
market_type: str | None = None,
|
|
1704
|
+
):
|
|
1705
|
+
market_type = market_type or self.market_type
|
|
1706
|
+
assert market_type is not None
|
|
1707
|
+
id = pos.symbol + market_type
|
|
1708
|
+
symbol = self._market_id.get(id)
|
|
1709
|
+
side = pos.positionSide.parse_to_position_side()
|
|
1710
|
+
signed_amount = Decimal(pos.positionAmt)
|
|
1711
|
+
|
|
1712
|
+
if not symbol:
|
|
1713
|
+
return
|
|
1714
|
+
|
|
1715
|
+
if signed_amount == 0:
|
|
1716
|
+
side = None
|
|
1717
|
+
else:
|
|
1718
|
+
if side == PositionSide.FLAT:
|
|
1719
|
+
if signed_amount > 0:
|
|
1720
|
+
side = PositionSide.LONG
|
|
1721
|
+
elif signed_amount < 0:
|
|
1722
|
+
side = PositionSide.SHORT
|
|
1723
|
+
|
|
1724
|
+
if isinstance(pos, BinancePortfolioMarginPositionRisk):
|
|
1725
|
+
unrealized_pnl = float(pos.unRealizedProfit)
|
|
1726
|
+
elif isinstance(pos, BinanceFuturesPositionInfo):
|
|
1727
|
+
unrealized_pnl = float(pos.unrealizedProfit)
|
|
1728
|
+
|
|
1729
|
+
position = Position(
|
|
1730
|
+
symbol=symbol,
|
|
1731
|
+
exchange=self._exchange_id,
|
|
1732
|
+
signed_amount=signed_amount,
|
|
1733
|
+
side=side,
|
|
1734
|
+
entry_price=float(pos.entryPrice),
|
|
1735
|
+
unrealized_pnl=unrealized_pnl,
|
|
1736
|
+
)
|
|
1737
|
+
if position.is_opened:
|
|
1738
|
+
self._cache._apply_position(position)
|
|
1739
|
+
|
|
1740
|
+
async def _init_account_balance(self):
|
|
1741
|
+
if (
|
|
1742
|
+
self._account_type.is_spot
|
|
1743
|
+
or self._account_type.is_isolated_margin_or_margin
|
|
1744
|
+
):
|
|
1745
|
+
res = await self._api_client.get_api_v3_account()
|
|
1746
|
+
elif self._account_type.is_linear:
|
|
1747
|
+
res = await self._api_client.get_fapi_v2_account()
|
|
1748
|
+
elif self._account_type.is_inverse:
|
|
1749
|
+
res = await self._api_client.get_dapi_v1_account()
|
|
1750
|
+
|
|
1751
|
+
if self._account_type.is_portfolio_margin:
|
|
1752
|
+
balances = []
|
|
1753
|
+
res_pm: list[
|
|
1754
|
+
BinancePortfolioMarginBalance
|
|
1755
|
+
] = await self._api_client.get_papi_v1_balance()
|
|
1756
|
+
for balance in res_pm:
|
|
1757
|
+
balances.extend(balance.parse_to_balances())
|
|
1758
|
+
else:
|
|
1759
|
+
balances = res.parse_to_balances()
|
|
1760
|
+
|
|
1761
|
+
self._cache._apply_balance(self._account_type, balances)
|
|
1762
|
+
|
|
1763
|
+
if self._account_type.is_linear or self._account_type.is_inverse:
|
|
1764
|
+
for pos in res.positions: # type: ignore
|
|
1765
|
+
self._apply_position(pos)
|
|
1766
|
+
|
|
1767
|
+
async def _init_position(self):
|
|
1768
|
+
# NOTE: Implement in `_init_account_balance`, only portfolio margin need to implement this
|
|
1769
|
+
if self._account_type.is_portfolio_margin:
|
|
1770
|
+
res_linear: list[
|
|
1771
|
+
BinancePortfolioMarginPositionRisk
|
|
1772
|
+
] = await self._api_client.get_papi_v1_um_position_risk()
|
|
1773
|
+
res_inverse: list[
|
|
1774
|
+
BinancePortfolioMarginPositionRisk
|
|
1775
|
+
] = await self._api_client.get_papi_v1_cm_position_risk()
|
|
1776
|
+
|
|
1777
|
+
for pos in res_linear:
|
|
1778
|
+
self._apply_position(pos, market_type="_linear")
|
|
1779
|
+
for pos in res_inverse:
|
|
1780
|
+
self._apply_position(pos, market_type="_inverse")
|
|
1781
|
+
|
|
1782
|
+
async def _position_mode_check(self):
|
|
1783
|
+
error_msg = "Please Set Position Mode to `One-Way Mode` in Binance App"
|
|
1784
|
+
|
|
1785
|
+
if self._account_type.is_linear:
|
|
1786
|
+
res = await self._api_client.get_fapi_v1_positionSide_dual()
|
|
1787
|
+
if res["dualSidePosition"]:
|
|
1788
|
+
raise PositionModeError(error_msg)
|
|
1789
|
+
|
|
1790
|
+
elif self._account_type.is_inverse:
|
|
1791
|
+
res = await self._api_client.get_dapi_v1_positionSide_dual()
|
|
1792
|
+
if res["dualSidePosition"]:
|
|
1793
|
+
raise PositionModeError(error_msg)
|
|
1794
|
+
|
|
1795
|
+
elif self._account_type.is_portfolio_margin:
|
|
1796
|
+
res_linear = await self._api_client.get_papi_v1_um_positionSide_dual()
|
|
1797
|
+
res_inverse = await self._api_client.get_papi_v1_cm_positionSide_dual()
|
|
1798
|
+
|
|
1799
|
+
if res_linear["dualSidePosition"]:
|
|
1800
|
+
raise PositionModeError(
|
|
1801
|
+
"Please Set Position Mode to `One-Way Mode` in Binance App for USD-M Future"
|
|
1802
|
+
)
|
|
1803
|
+
|
|
1804
|
+
if res_inverse["dualSidePosition"]:
|
|
1805
|
+
raise PositionModeError(
|
|
1806
|
+
"Please Set Position Mode to `One-Way Mode` in Binance App for Coin-M Future"
|
|
1807
|
+
)
|