hyperquant 1.48__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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

Files changed (42) hide show
  1. hyperquant/__init__.py +8 -0
  2. hyperquant/broker/auth.py +972 -0
  3. hyperquant/broker/bitget.py +311 -0
  4. hyperquant/broker/bitmart.py +720 -0
  5. hyperquant/broker/coinw.py +487 -0
  6. hyperquant/broker/deepcoin.py +651 -0
  7. hyperquant/broker/edgex.py +500 -0
  8. hyperquant/broker/hyperliquid.py +570 -0
  9. hyperquant/broker/lbank.py +661 -0
  10. hyperquant/broker/lib/edgex_sign.py +455 -0
  11. hyperquant/broker/lib/hpstore.py +252 -0
  12. hyperquant/broker/lib/hyper_types.py +48 -0
  13. hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
  14. hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
  15. hyperquant/broker/lib/util.py +22 -0
  16. hyperquant/broker/lighter.py +679 -0
  17. hyperquant/broker/models/apexpro.py +150 -0
  18. hyperquant/broker/models/bitget.py +359 -0
  19. hyperquant/broker/models/bitmart.py +635 -0
  20. hyperquant/broker/models/coinw.py +724 -0
  21. hyperquant/broker/models/deepcoin.py +809 -0
  22. hyperquant/broker/models/edgex.py +1053 -0
  23. hyperquant/broker/models/hyperliquid.py +284 -0
  24. hyperquant/broker/models/lbank.py +557 -0
  25. hyperquant/broker/models/lighter.py +868 -0
  26. hyperquant/broker/models/ourbit.py +1155 -0
  27. hyperquant/broker/models/polymarket.py +1071 -0
  28. hyperquant/broker/ourbit.py +550 -0
  29. hyperquant/broker/polymarket.py +2399 -0
  30. hyperquant/broker/ws.py +132 -0
  31. hyperquant/core.py +513 -0
  32. hyperquant/datavison/_util.py +18 -0
  33. hyperquant/datavison/binance.py +111 -0
  34. hyperquant/datavison/coinglass.py +237 -0
  35. hyperquant/datavison/okx.py +177 -0
  36. hyperquant/db.py +191 -0
  37. hyperquant/draw.py +1200 -0
  38. hyperquant/logkit.py +205 -0
  39. hyperquant/notikit.py +124 -0
  40. hyperquant-1.48.dist-info/METADATA +32 -0
  41. hyperquant-1.48.dist-info/RECORD +42 -0
  42. hyperquant-1.48.dist-info/WHEEL +4 -0
@@ -0,0 +1,720 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import random
6
+ import time
7
+ from typing import Any, Literal, Optional, Sequence
8
+
9
+ import pybotters
10
+
11
+ from .models.bitmart import BitmartDataStore
12
+
13
+
14
+ class Book():
15
+ def __init__(self):
16
+ self.limit: int | None = None
17
+ self.store = {}
18
+
19
+ def on_message(self, msg: dict[str, Any], ws=None) -> None:
20
+ data = msg.get("data")
21
+ if not isinstance(data, dict):
22
+ return
23
+ symbol = data.get("symbol")
24
+ self.store[symbol] = data
25
+
26
+ def find(self, query: dict[str, Any]) -> dict[str, Any] | None:
27
+ s = query.get("s")
28
+ S = query.get("S")
29
+ item = self.store.get(s)
30
+ if item:
31
+ if S == "a":
32
+ return [{"s": s, "S": "a", "p": item["asks"][0]['price'], "q": item["asks"][0]['vol']}]
33
+ elif S == "b":
34
+ return [{"s": s, "S": "b", "p": item["bids"][0]['price'], "q": item["bids"][0]['vol']}]
35
+ else:
36
+ return []
37
+
38
+ class BitmartDataStore2(BitmartDataStore):
39
+ def _init(self):
40
+ self.bk = Book()
41
+ return super()._init()
42
+
43
+ @property
44
+ def book(self) -> Book:
45
+ return self.bk
46
+
47
+
48
+ class Bitmart:
49
+ """Bitmart 合约交易(REST + WebSocket)。"""
50
+
51
+ def __init__(
52
+ self,
53
+ client: pybotters.Client,
54
+ *,
55
+ public_api: str | None = None,
56
+ forward_api: str | None = None,
57
+ ws_url: str | None = None,
58
+ account_index: int | None = None,
59
+ apis: str = None
60
+ ) -> None:
61
+ self.client = client
62
+ self.store = BitmartDataStore2()
63
+
64
+ self.public_api = public_api or "https://contract-v2.bitmart.com"
65
+ self.private_api = "https://derivatives.bitmart.com"
66
+ self.forward_api = f'{self.private_api}/gw-api/contract-tiger/forward'
67
+ self.ws_url = ws_url or "wss://contract-ws-v2.bitmart.com/v1/ifcontract/realTime"
68
+ self.api_ws_url = "wss://openapi-ws-v2.bitmart.com/api?protocol=1.1"
69
+ self.api_url = "https://api-cloud-v2.bitmart.com"
70
+ self.account_index = account_index
71
+ self.apis = apis
72
+ self.symbol_to_contract_id: dict[str, str] = {}
73
+ self.book = Book()
74
+
75
+ async def __aenter__(self) -> "Bitmart":
76
+ await self.update("detail")
77
+ asyncio.create_task(self.auto_refresh())
78
+
79
+ for entry in self.store.detail.find():
80
+ contract_id = entry.get("contract_id")
81
+ symbol = entry.get("name") or entry.get("display_name")
82
+ if contract_id is None or symbol is None:
83
+ continue
84
+ self.symbol_to_contract_id[str(symbol)] = str(contract_id)
85
+
86
+ return self
87
+
88
+ async def auto_refresh(self, sec=3600, test=False) -> None:
89
+ """每隔一小时刷新token"""
90
+ client = self.client
91
+ while not client._session.closed:
92
+
93
+ await asyncio.sleep(sec)
94
+
95
+ if client._session.__dict__["_apis"].get("bitmart") is None:
96
+ continue
97
+
98
+ # 执行请求
99
+ res = await client.post(
100
+ f"{self.private_api}/gw-api/gateway/token/v2/renew",
101
+ )
102
+
103
+ print(await res.text())
104
+ resp:dict = await res.json()
105
+ if resp.get("success") is False:
106
+ raise ValueError(f"Bitmart refreshToken error: {resp}")
107
+
108
+ data:dict = resp.get("data", {})
109
+ new_token = data.get("accessToken")
110
+ secret = data.get("accessSalt")
111
+
112
+ # 加载原来的apis
113
+ apis_dict = client._load_apis(self.apis)
114
+
115
+ device = apis_dict['bitmart'][2]
116
+
117
+ apis_dict["bitmart"] = [new_token, secret, device]
118
+
119
+ client._session.__dict__["_apis"] = client._encode_apis(apis_dict)
120
+
121
+ if test:
122
+ print("Bitmart token refreshed.")
123
+ break
124
+
125
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
126
+ return None
127
+
128
+ def get_contract_id(self, symbol: str) -> str | None:
129
+ """Resolve contract ID from cached detail data."""
130
+ detail = (
131
+ self.store.detail.get({"name": symbol})
132
+ or self.store.detail.get({"display_name": symbol})
133
+ or self.store.detail.get({"contract_id": symbol})
134
+ )
135
+ if detail is None:
136
+ return None
137
+ contract_id = detail.get("contract_id")
138
+ if contract_id is None:
139
+ return None
140
+ return str(contract_id)
141
+
142
+ def _get_detail_entry(
143
+ self,
144
+ *,
145
+ symbol: str | None = None,
146
+ market_index: int | None = None,
147
+ ) -> dict[str, Any] | None:
148
+ if symbol:
149
+ entry = (
150
+ self.store.detail.get({"name": symbol})
151
+ or self.store.detail.get({"display_name": symbol})
152
+ )
153
+ if entry:
154
+ return entry
155
+
156
+ if market_index is not None:
157
+ entries = self.store.detail.find({"contract_id": market_index})
158
+ if entries:
159
+ return entries[0]
160
+ entries = self.store.detail.find({"contract_id": str(market_index)})
161
+ if entries:
162
+ return entries[0]
163
+
164
+ return None
165
+
166
+ @staticmethod
167
+ def _normalize_enum(
168
+ value: int | str,
169
+ mapping: dict[str, int],
170
+ field: str,
171
+ ) -> int:
172
+ if isinstance(value, str):
173
+ key = value.lower()
174
+ try:
175
+ return mapping[key]
176
+ except KeyError as exc:
177
+ raise ValueError(f"Unsupported {field}: {value}") from exc
178
+ try:
179
+ return int(value)
180
+ except (TypeError, ValueError) as exc:
181
+ raise ValueError(f"Unsupported {field}: {value}") from exc
182
+
183
+ async def update(
184
+ self,
185
+ update_type: Literal[
186
+ "detail",
187
+ "orders",
188
+ "positions",
189
+ "balances",
190
+ "account",
191
+ "all",
192
+ "history_orders",
193
+ "ticker",
194
+ ] = "all",
195
+ *,
196
+ orders_params: dict[str, Any] | None = None,
197
+ positions_params: dict[str, Any] | None = None,
198
+ ) -> None:
199
+ """Refresh cached REST resources."""
200
+
201
+ tasks: dict[str, Any] = {}
202
+
203
+ include_detail = update_type in {"detail", "all"}
204
+ include_orders = update_type in {"orders", "all"}
205
+ include_positions = update_type in {"positions", "all"}
206
+ include_balances = update_type in {"balances", "account", "all"}
207
+ include_history_orders = update_type in {"history_orders"}
208
+ include_ticker = update_type in {"ticker", "all"}
209
+
210
+ if include_detail:
211
+ tasks["detail"] = self.client.get(f"{self.public_api}/v1/ifcontract/contracts_all")
212
+
213
+ if include_orders:
214
+ params = {
215
+ "status": 3,
216
+ "size": 200,
217
+ "orderType": 0,
218
+ "offset": 0,
219
+ "direction": 0,
220
+ "type": 1,
221
+ }
222
+ if orders_params:
223
+ params.update(orders_params)
224
+ tasks["orders"] = self.client.get(
225
+ f"{self.forward_api}/v1/ifcontract/userAllOrders",
226
+ params=params,
227
+ )
228
+
229
+ if include_positions:
230
+ params = {"status": 1}
231
+ if positions_params:
232
+ params.update(positions_params)
233
+ tasks["positions"] = self.client.get(
234
+ f"{self.forward_api}/v1/ifcontract/userPositions",
235
+ params=params,
236
+ )
237
+
238
+ if include_balances:
239
+ tasks["balances"] = self.client.get(
240
+ f"{self.forward_api}/v1/ifcontract/copy/trade/user/info",
241
+ )
242
+
243
+ if include_history_orders:
244
+ d_params = {"offset": 0, "status": 60, "size": 20, "type": 1}
245
+ d_params.update(orders_params or {})
246
+ tasks["history_orders"] = self.client.get(
247
+ f"{self.forward_api}/v1/ifcontract/userAllOrders",
248
+ params=d_params,
249
+ )
250
+
251
+ if include_ticker:
252
+ tasks["ticker"] = self.client.get(
253
+ f"{self.public_api}/v1/ifcontract/tickers"
254
+ )
255
+
256
+ if not tasks:
257
+ raise ValueError(f"Unsupported update_type: {update_type}")
258
+
259
+ results: dict[str, Any] = {}
260
+ for key, req in tasks.items():
261
+ res = await req
262
+ if res.content_type and "json" in res.content_type:
263
+ results[key] = await res.json()
264
+ else:
265
+ text = await res.text()
266
+ try:
267
+ results[key] = json.loads(text)
268
+ except json.JSONDecodeError as exc:
269
+ raise ValueError(
270
+ f"Unexpected response format for {key}: {res.content_type} {text[:200]}"
271
+ ) from exc
272
+
273
+ if "detail" in results:
274
+ resp = results["detail"]
275
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
276
+ raise ValueError(f"Bitmart detail API error: {resp}")
277
+ self.store.detail._onresponse(resp)
278
+ for entry in self.store.detail.find():
279
+ contract_id = entry.get("contract_id")
280
+ symbol = entry.get("name") or entry.get("display_name")
281
+ if contract_id is None or symbol is None:
282
+ continue
283
+
284
+ if "orders" in results:
285
+ resp = results["orders"]
286
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
287
+ raise ValueError(f"Bitmart orders API error: {resp}")
288
+ self.store.orders._onresponse(resp)
289
+
290
+ if "positions" in results:
291
+ resp = results["positions"]
292
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
293
+ raise ValueError(f"Bitmart positions API error: {resp}")
294
+ self.store.positions._onresponse(resp)
295
+
296
+ if "balances" in results:
297
+ resp = results["balances"]
298
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
299
+ raise ValueError(f"Bitmart balances API error: {resp}")
300
+ self.store.balances._onresponse(resp)
301
+
302
+ if "ticker" in results:
303
+ resp = results["ticker"]
304
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
305
+ raise ValueError(f"Bitmart ticker API error: {resp}")
306
+ self.store.ticker._onresponse(resp)
307
+
308
+ if "history_orders" in results:
309
+ resp = results["history_orders"]
310
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
311
+ raise ValueError(f"Bitmart history_orders API error: {resp}")
312
+ self.store.orders._onresponse(resp)
313
+
314
+ async def sub_orderbook(
315
+ self,
316
+ symbols: Sequence[str] | str,
317
+ *,
318
+ depth_limit: int | None = None,
319
+ ) -> pybotters.ws.WebSocketApp:
320
+ """Subscribe order book channel(s)."""
321
+
322
+ if isinstance(symbols, str):
323
+ symbols = [symbols]
324
+
325
+
326
+ if not symbols:
327
+ raise ValueError("symbols must not be empty")
328
+ if depth_limit is not None:
329
+ self.store.book.limit = depth_limit
330
+
331
+ hdlr_json = self.store.book.on_message
332
+
333
+ channels: list[str] = []
334
+ for symbol in symbols:
335
+ channels.append(f"futures/depthAll5:{symbol}@100ms")
336
+
337
+ if not channels:
338
+ raise ValueError("No channels resolved for subscription")
339
+
340
+ payload = {"action": "subscribe", "args": channels}
341
+
342
+ ws_app = self.client.ws_connect(
343
+ self.api_ws_url,
344
+ send_json=payload,
345
+ hdlr_json=hdlr_json,
346
+ autoping=False,
347
+ )
348
+
349
+ await ws_app._event.wait()
350
+ return ws_app
351
+
352
+
353
+ def gen_order_id(self):
354
+ ts = int(time.time() * 1000) # 13位毫秒时间戳
355
+ rand = random.randint(100000, 999999) # 6位随机数
356
+ return int(f"{ts}{rand}")
357
+
358
+ async def place_order(
359
+ self,
360
+ symbol: str,
361
+ *,
362
+ category: Literal[1,2,"limit","market"] = "limit",
363
+ price: float,
364
+ qty: Optional[float] = None,
365
+ qty_contract: Optional[int] = None,
366
+ side: Literal[1, 2, 3, 4, "open_long", "close_short", "close_long", "open_short", "buy", "sell"] = "open_long",
367
+ mode: Literal[1, 2, 3, 4, "gtc", "ioc", "fok", "maker_only", "maker-only", "post_only"] = "gtc",
368
+ open_type: Literal[1, 2, "cross", "isolated"] = "isolated",
369
+ leverage: int | str = 10,
370
+ reverse_vol: int | float = 0,
371
+ trigger_price: float | None = None,
372
+ custom_id: int | str | None = None,
373
+ extra_params: dict[str, Any] | None = None,
374
+ use_api: bool = False,
375
+ ) -> int:
376
+ """Submit an order via ``submitOrder``.
377
+ 返回值: order_id (int)
378
+ """
379
+ if qty is None and qty_contract is None:
380
+ raise ValueError("Either qty or qty_contract must be provided.")
381
+
382
+ contract_id = self.get_contract_id(symbol)
383
+ if contract_id is None:
384
+ raise ValueError(f"Unknown symbol: {symbol}")
385
+ contract_id_int = int(contract_id)
386
+
387
+ detail = self._get_detail_entry(symbol=symbol, market_index=contract_id_int)
388
+ if detail is None:
389
+ await self.update("detail")
390
+ detail = self._get_detail_entry(symbol=symbol, market_index=contract_id_int)
391
+ if detail is None:
392
+ raise ValueError(f"Market metadata unavailable for symbol: {symbol}")
393
+
394
+ if qty is not None:
395
+
396
+ contract_size_str = detail.get("contract_size") or detail.get("vol_unit") or "1"
397
+ try:
398
+ contract_size_val = float(contract_size_str)
399
+ except (TypeError, ValueError):
400
+ contract_size_val = 1.0
401
+ if contract_size_val <= 0:
402
+ raise ValueError(f"Invalid contract_size for {symbol}: {contract_size_str}")
403
+
404
+ contracts_float = float(qty) / contract_size_val
405
+ contracts_int = int(round(contracts_float))
406
+ if contracts_int <= 0:
407
+ raise ValueError(
408
+ f"Volume too small for contract size ({contract_size_val}): volume={qty}"
409
+ )
410
+
411
+ if qty_contract is not None:
412
+ contracts_int = int(qty_contract)
413
+ if contracts_int <= 0:
414
+ raise ValueError(f"Volume must be positive integer contracts: volume={qty_contract}")
415
+
416
+ price_unit = detail.get("price_unit") or 1
417
+ try:
418
+ price_unit_val = float(price_unit)
419
+ except (TypeError, ValueError):
420
+ price_unit_val = 1.0
421
+ if price_unit_val <= 0:
422
+ price_unit_val = 1.0
423
+
424
+ price_value = float(price)
425
+ adjusted_price = int(price_value / price_unit_val) * price_unit_val
426
+
427
+ category = self._normalize_enum(
428
+ category,
429
+ {
430
+ "limit": 1,
431
+ "market": 2,
432
+ },
433
+ "category",
434
+ )
435
+
436
+ if category == 2: # market
437
+ adjusted_price = 0.0
438
+ price_fmt = f"{adjusted_price:.15f}".rstrip("0").rstrip(".") or "0"
439
+
440
+ way_value = self._normalize_enum(
441
+ side,
442
+ {
443
+ "open_long": 1,
444
+ "close_short": 2,
445
+ "close_long": 3,
446
+ "open_short": 4,
447
+ "buy": 1,
448
+ "sell": 4,
449
+ },
450
+ "way",
451
+ )
452
+ mode_value = self._normalize_enum(
453
+ mode,
454
+ {
455
+ "gtc": 1,
456
+ "fok": 2,
457
+ "ioc": 3,
458
+ "maker_only": 4,
459
+ "maker-only": 4,
460
+ "post_only": 4,
461
+ },
462
+ "mode",
463
+ )
464
+ open_type_value = self._normalize_enum(
465
+ open_type,
466
+ {
467
+ "cross": 1,
468
+ "isolated": 2,
469
+ },
470
+ "open_type",
471
+ )
472
+
473
+ if use_api:
474
+ # Official API path
475
+ order_type_str = "limit" if category == 1 else "market"
476
+ open_type_str = "cross" if open_type_value == 1 else "isolated"
477
+ client_oid = str(custom_id or self.gen_order_id())
478
+ api_payload: dict[str, Any] = {
479
+ "symbol": symbol,
480
+ "client_order_id": client_oid,
481
+ "side": way_value,
482
+ "type": order_type_str,
483
+ "mode": mode_value,
484
+ "leverage": str(leverage),
485
+ "open_type": open_type_str,
486
+ "size": int(contracts_int),
487
+ }
488
+ if order_type_str == "limit":
489
+ api_payload["price"] = price_fmt
490
+ if extra_params:
491
+ api_payload.update(extra_params)
492
+ # Ensure leverage is synchronized via official API before placing order
493
+ try:
494
+ lev_payload = {
495
+ "symbol": symbol,
496
+ "leverage": str(leverage),
497
+ "open_type": open_type_str,
498
+ }
499
+ res_lev = await self.client.post(
500
+ f"{self.api_url}/contract/private/submit-leverage",
501
+ json=lev_payload,
502
+ )
503
+ txt_lev = await res_lev.text()
504
+ try:
505
+ resp_lev = json.loads(txt_lev)
506
+ if resp_lev.get("code") != 1000:
507
+ # ignore and proceed; order may still pass
508
+ pass
509
+ except json.JSONDecodeError:
510
+ pass
511
+ await asyncio.sleep(0.05)
512
+ except Exception:
513
+ pass
514
+
515
+ res = await self.client.post(
516
+ f"{self.api_url}/contract/private/submit-order",
517
+ json=api_payload,
518
+ )
519
+ # Parse response (some errors may return text/plain containing JSON)
520
+ text = await res.text()
521
+ try:
522
+ resp = json.loads(text)
523
+ except json.JSONDecodeError:
524
+ raise ValueError(f"Bitmart API submit-order non-json response: {text[:200]}")
525
+ if resp.get("code") != 1000:
526
+ # Auto-sync leverage once if required, then retry once
527
+ if resp.get("code") in (40012,):
528
+ try:
529
+ # Retry leverage sync via official API then retry the order
530
+ lev_payload = {
531
+ "symbol": symbol,
532
+ "leverage": str(leverage),
533
+ "open_type": open_type_str,
534
+ }
535
+ await self.client.post(
536
+ f"{self.api_url}/contract/private/submit-leverage",
537
+ json=lev_payload,
538
+ )
539
+ await asyncio.sleep(0.05)
540
+ res2 = await self.client.post(
541
+ f"{self.api_url}/contract/private/submit-order",
542
+ json=api_payload,
543
+ )
544
+ text2 = await res2.text()
545
+ try:
546
+ resp2 = json.loads(text2)
547
+ except json.JSONDecodeError:
548
+ raise ValueError(
549
+ f"Bitmart API submit-order non-json response: {text2[:200]}"
550
+ )
551
+ if resp2.get("code") == 1000:
552
+ return resp2.get("data", {}).get("order_id")
553
+ else:
554
+ raise ValueError(f"Bitmart API submit-order error: {resp2}")
555
+ except Exception:
556
+ # Fall through to raise original error if sync failed
557
+ pass
558
+ raise ValueError(f"Bitmart API submit-order error: {resp}")
559
+ return resp.get("data", {}).get("order_id")
560
+ else:
561
+ payload: dict[str, Any] = {
562
+ "place_all_order": False,
563
+ "contract_id": contract_id_int,
564
+ "category": category,
565
+ "price": price_fmt,
566
+ "vol": contracts_int,
567
+ "way": way_value,
568
+ "mode": mode_value,
569
+ "open_type": open_type_value,
570
+ "leverage": leverage,
571
+ "reverse_vol": reverse_vol,
572
+ }
573
+
574
+ if trigger_price is not None:
575
+ payload["trigger_price"] = trigger_price
576
+
577
+ payload["custom_id"] = custom_id or self.gen_order_id()
578
+
579
+ if extra_params:
580
+ payload.update(extra_params)
581
+
582
+ res = await self.client.post(
583
+ f"{self.forward_api}/v1/ifcontract/submitOrder",
584
+ json=payload,
585
+ )
586
+ resp = await res.json()
587
+
588
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
589
+ raise ValueError(f"Bitmart submitOrder error: {resp}")
590
+ return resp.get("data", {}).get("order_id")
591
+
592
+ async def cancel_order(
593
+ self,
594
+ symbol: str,
595
+ order_ids: Sequence[int | str],
596
+ *,
597
+ nonce: int | None = None,
598
+ ) -> dict[str, Any]:
599
+ """Cancel one or multiple orders."""
600
+
601
+ contract_id = self.get_contract_id(symbol)
602
+ if contract_id is None:
603
+ raise ValueError(f"Unknown symbol: {symbol}")
604
+
605
+ payload = {
606
+ "orders": [
607
+ {
608
+ "contract_id": int(contract_id),
609
+ "orders": [int(order_id) for order_id in order_ids],
610
+ }
611
+ ],
612
+ "nonce": nonce if nonce is not None else int(time.time()),
613
+ }
614
+
615
+ res = await self.client.post(
616
+ f"{self.forward_api}/v1/ifcontract/cancelOrders",
617
+ json=payload,
618
+ )
619
+ resp = await res.json()
620
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
621
+ raise ValueError(f"Bitmart cancelOrders error: {resp}")
622
+ return resp
623
+
624
+ async def get_leverage(
625
+ self,
626
+ *,
627
+ symbol: str | None = None,
628
+ contract_id: int | str | None = None,
629
+ ) -> dict[str, Any]:
630
+ """
631
+ 获取指定合约的杠杆信息(可通过 contract_id 或 symbol 查询)。
632
+
633
+ 参数:
634
+ symbol (str | None): 合约符号,例如 "BTCUSDT"。如果未传入 contract_id,则会自动解析。
635
+ contract_id (int | str | None): 合约 ID,可直接指定。
636
+
637
+ 返回:
638
+ dict[str, Any]: 杠杆信息字典,典型返回结构如下:
639
+ {
640
+ "contract_id": 1,
641
+ "leverage": 96, # 当前杠杆倍数
642
+ "open_type": 2, # 开仓类型 (1=全仓, 2=逐仓)
643
+ "max_leverage": {
644
+ "contract_id": 1,
645
+ "leverage": "200", # 最大可用杠杆倍数
646
+ "open_type": 0,
647
+ "imr": "0.005", # 初始保证金率
648
+ "mmr": "0.0025", # 维持保证金率
649
+ "value": "0"
650
+ }
651
+ }
652
+
653
+ 异常:
654
+ ValueError: 当未提供 symbol 或 contract_id,或接口返回错误时抛出。
655
+
656
+ 示例:
657
+ data = await bitmart.get_leverage(symbol="BTCUSDT")
658
+ print(data["leverage"]) # 输出当前杠杆倍数
659
+ """
660
+ if contract_id is None:
661
+ if symbol is not None:
662
+ contract_id = self.get_contract_id(symbol)
663
+ if contract_id is None:
664
+ raise ValueError("Either contract_id or a valid symbol must be provided to get leverage info.")
665
+ res = await self.client.get(
666
+ f"{self.forward_api}/v1/ifcontract/getLeverage",
667
+ params={"contract_id": contract_id},
668
+ )
669
+ resp = await res.json()
670
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
671
+ raise ValueError(f"Bitmart getLeverage error: {resp}")
672
+ return resp.get("data")
673
+
674
+ async def bind_leverage(
675
+ self,
676
+ *,
677
+ symbol: str | None = None,
678
+ contract_id: int | str | None = None,
679
+ leverage: int | str,
680
+ open_type: Literal[1, 2] = 2,
681
+ ) -> None:
682
+ """
683
+ 绑定(设置)指定合约的杠杆倍数。
684
+
685
+ 参数:
686
+ symbol (str | None): 合约符号,例如 "BTCUSDT"。若未传入 contract_id,会自动解析。
687
+ contract_id (int | str | None): 合约 ID,可直接指定。
688
+ leverage (int | str): 要设置的杠杆倍数,如 20、50、100。
689
+ open_type (int): 开仓模式,1=全仓(Cross),2=逐仓(Isolated)。
690
+
691
+ 返回:
692
+ None — 如果接口调用成功,不返回任何内容。
693
+ 若失败则抛出 ValueError。
694
+
695
+ 异常:
696
+ ValueError: 当未提供 symbol 或 contract_id,或接口返回错误时抛出。
697
+
698
+ 示例:
699
+ await bitmart.bind_leverage(symbol="BTCUSDT", leverage=50, open_type=2)
700
+ """
701
+ if contract_id is None:
702
+ if symbol is not None:
703
+ contract_id = self.get_contract_id(symbol)
704
+ if contract_id is None:
705
+ raise ValueError("Either contract_id or a valid symbol must be provided to bind leverage.")
706
+
707
+ payload = {
708
+ "contract_id": int(contract_id),
709
+ "leverage": leverage,
710
+ "open_type": open_type,
711
+ }
712
+
713
+ res = await self.client.post(
714
+ f"{self.forward_api}/v1/ifcontract/bindLeverage",
715
+ json=payload,
716
+ )
717
+ resp = await res.json()
718
+ if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
719
+ raise ValueError(f"Bitmart bindLeverage error: {resp}")
720
+ return None