hyperquant 1.21__tar.gz → 1.25__tar.gz

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 (66) hide show
  1. {hyperquant-1.21 → hyperquant-1.25}/PKG-INFO +1 -3
  2. {hyperquant-1.21 → hyperquant-1.25}/apis.json +5 -0
  3. {hyperquant-1.21 → hyperquant-1.25}/pyproject.toml +1 -3
  4. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/auth.py +101 -1
  5. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/bitmart.py +170 -80
  6. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lighter.py +222 -12
  7. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/bitmart.py +14 -25
  8. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/lighter.py +360 -0
  9. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/ws.py +7 -0
  10. hyperquant-1.25/test.py +17 -0
  11. {hyperquant-1.21 → hyperquant-1.25}/tests/test_bitmart.py +98 -26
  12. hyperquant-1.25/tests/test_lighter.py +181 -0
  13. hyperquant-1.25/tests/test_store.py +182 -0
  14. {hyperquant-1.21 → hyperquant-1.25}/uv.lock +159 -220
  15. hyperquant-1.21/src/hyperquant/broker/coinup.py +0 -591
  16. hyperquant-1.21/src/hyperquant/broker/models/coinup.py +0 -334
  17. hyperquant-1.21/test.py +0 -48
  18. hyperquant-1.21/tests/test_lighter.py +0 -116
  19. {hyperquant-1.21 → hyperquant-1.25}/.gitignore +0 -0
  20. {hyperquant-1.21 → hyperquant-1.25}/.python-version +0 -0
  21. {hyperquant-1.21 → hyperquant-1.25}/README.md +0 -0
  22. {hyperquant-1.21 → hyperquant-1.25}/data/alpine_smoke.log +0 -0
  23. {hyperquant-1.21 → hyperquant-1.25}/data/logs/notikit.log +0 -0
  24. {hyperquant-1.21 → hyperquant-1.25}/data/logs/test_order_sync.log +0 -0
  25. {hyperquant-1.21 → hyperquant-1.25}/data/records_swap.csv +0 -0
  26. {hyperquant-1.21 → hyperquant-1.25}/data/records_swapc.csv +0 -0
  27. {hyperquant-1.21 → hyperquant-1.25}/doc/bitmart.md +0 -0
  28. {hyperquant-1.21 → hyperquant-1.25}/doc/coinup.md +0 -0
  29. {hyperquant-1.21 → hyperquant-1.25}/doc/lbank.md +0 -0
  30. {hyperquant-1.21 → hyperquant-1.25}/pub.sh +0 -0
  31. {hyperquant-1.21 → hyperquant-1.25}/requirements-dev.lock +0 -0
  32. {hyperquant-1.21 → hyperquant-1.25}/requirements.lock +0 -0
  33. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/__init__.py +0 -0
  34. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/bitget.py +0 -0
  35. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/coinw.py +0 -0
  36. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/edgex.py +0 -0
  37. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/hyperliquid.py +0 -0
  38. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lbank.py +0 -0
  39. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  40. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/hpstore.py +0 -0
  41. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  42. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/util.py +0 -0
  43. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/bitget.py +0 -0
  44. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/coinw.py +0 -0
  45. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/edgex.py +0 -0
  46. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  47. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/lbank.py +0 -0
  48. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/ourbit.py +0 -0
  49. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/ourbit.py +0 -0
  50. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/core.py +0 -0
  51. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/_util.py +0 -0
  52. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/binance.py +0 -0
  53. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/coinglass.py +0 -0
  54. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/okx.py +0 -0
  55. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/db.py +0 -0
  56. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/draw.py +0 -0
  57. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/logkit.py +0 -0
  58. {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/notikit.py +0 -0
  59. {hyperquant-1.21 → hyperquant-1.25}/tests/test_bitget.py +0 -0
  60. {hyperquant-1.21 → hyperquant-1.25}/tests/test_coinup.py +0 -0
  61. {hyperquant-1.21 → hyperquant-1.25}/tests/test_coinw.py +0 -0
  62. {hyperquant-1.21 → hyperquant-1.25}/tests/test_draw.py +0 -0
  63. {hyperquant-1.21 → hyperquant-1.25}/tests/test_edgex.py +0 -0
  64. {hyperquant-1.21 → hyperquant-1.25}/tests/test_lbank.py +0 -0
  65. {hyperquant-1.21 → hyperquant-1.25}/tests/test_ourbit.py +0 -0
  66. {hyperquant-1.21 → hyperquant-1.25}/tests/tmp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.21
3
+ Version: 1.25
4
4
  Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
5
5
  Project-URL: Homepage, https://github.com/yourusername/hyperquant
6
6
  Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
@@ -16,14 +16,12 @@ Requires-Python: >=3.11
16
16
  Requires-Dist: aiohttp>=3.10.4
17
17
  Requires-Dist: colorama>=0.4.6
18
18
  Requires-Dist: cryptography>=44.0.2
19
- Requires-Dist: curl-cffi>=0.13.0
20
19
  Requires-Dist: duckdb>=1.2.2
21
20
  Requires-Dist: lighter-sdk
22
21
  Requires-Dist: numpy>=1.21.0
23
22
  Requires-Dist: pandas>=2.2.3
24
23
  Requires-Dist: pybotters>=1.9.1
25
24
  Requires-Dist: pyecharts>=2.0.8
26
- Requires-Dist: rnet==3.0.0rc10
27
25
  Description-Content-Type: text/markdown
28
26
 
29
27
  # minquant
@@ -27,5 +27,10 @@
27
27
  "eyJoZWFkZXIiOnsidHlwIjoiQml0TWFydCIsImFsZyI6IkJNQVBJU0lYIn0sInBheWxvYWQiOnsiamlkIjoiMjk4Mjk1NWMzZTBlNDA5YmFjODYyZGNlOWY0MTFhNDkiLCJ2ZXJzaW9uIjoiMjAyNTExMDEiLCJleHBpcmVzQXQiOjE3NjIwNTE3MzI0NTMsImJtIjoiRVhFbjk2UzV2dGRISmtKZ0hYejlPcTA0a0ZTdFdHTWtRWG5yVHpDZktYdW1OOHFIVTh0WExUeXlTaEMzVFJDMlZHOVhuMWxkcVkzdVdlOHluWlBrTGhqQWNIT0VjK2VoeEhlc3VJVUh3U3ArRVFnMEFtY0JIWW53OVRuelRkb2Fob25jakxBNTdtRDVEK2tPSHNWUXdDdElXRGNsTWdpd1d3V3NlY0d5Y1hNdjJJd1pvUVAwZzRlZUtudTgwNDlpZ1RPdERkN0JLenlFN0tONy85ZWpBL1hHQmg4SzBROW9hTmN6aWwwMmxVUnBGNm9wUUJHVTY1eHhBTkMwR0Zyb2RZMmFNalRtQVN2Si9vdmJMNEQxZGtUcUp6azIxdVlhamllRm9OTmREdlZqMXlzOGdTTGJBc3FWMDlsd2x0M3lTWER2L093a0hyclhJMlAyZVZqU2JuK1hPODhKRTlFYWRpTkcwRU9EOWhLRy9YRFhoU0dVT1N5YnJrNWdLdjNoIn0sInNpZ25hdHVyZSI6InQ3SmRFZ3VhTzNrUU9RSlNBcVRqMDVQRCsrcGR6UjlLVDVZTGhNdmNQRDlvc0tWcjRYMEM0VjkwaStYTm0rMFBxNlhuZGo5SjFSeXFtZVBuVG5QUmRBPT0ifQ==",
28
28
  "4OamVUwuSznwyStlmRIfc9o0y5tCYdMOh",
29
29
  "1c0886dd192a1dd0f23f71f7ab577a45"
30
+ ],
31
+ "bitmart_api":[
32
+ "21c0e5173835d02805b99641c807375866c41275",
33
+ "c82fbdb564f194362278b69370c47116db00165fc6a7f5ef770158fcb73a2c70",
34
+ "api"
30
35
  ]
31
36
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "1.21"
3
+ version = "1.25"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -14,8 +14,6 @@ dependencies = [
14
14
  "numpy>=1.21.0", # Added numpy as a new dependency
15
15
  "duckdb>=1.2.2",
16
16
  "pybotters>=1.9.1",
17
- "curl-cffi>=0.13.0",
18
- "rnet==3.0.0rc10",
19
17
  "lighter-sdk",
20
18
  ]
21
19
  readme = "README.md"
@@ -249,6 +249,102 @@ class Auth:
249
249
 
250
250
  return args
251
251
 
252
+ @staticmethod
253
+ def bitmart_api(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
254
+ """Bitmart OpenAPI (futures v2) signing for api-cloud-v2.bitmart.com.
255
+
256
+ Spec per docs:
257
+ X-BM-SIGN = hex_lower(HMAC_SHA256(secret, timestamp + '#' + memo + '#' + payload_string))
258
+ - For POST: payload_string is the JSON body string
259
+ - For GET: payload_string is the query string (if any), otherwise empty
260
+ Headers required: X-BM-KEY, X-BM-TIMESTAMP, X-BM-SIGN
261
+ """
262
+ method: str = args[0]
263
+ url: URL = args[1]
264
+ headers: CIMultiDict = kwargs["headers"]
265
+
266
+ session = kwargs["session"]
267
+ try:
268
+ api_name = pybotters.auth.Hosts.items[url.host].name
269
+ except (KeyError, AttributeError):
270
+ api_name = url.host
271
+
272
+ creds = session.__dict__.get("_apis", {}).get(api_name)
273
+ if not creds or len(creds) < 3:
274
+ raise RuntimeError("Bitmart API credentials (access_key, secret, memo) are required")
275
+
276
+ access_key = creds[0]
277
+ secret = creds[1]
278
+ memo = creds[2]
279
+ if isinstance(secret, str):
280
+ secret_bytes = secret.encode("utf-8")
281
+ else:
282
+ secret_bytes = secret
283
+ if isinstance(memo, bytes):
284
+ memo = memo.decode("utf-8")
285
+
286
+ timestamp = str(int(time.time() * 1000))
287
+ method_upper = method.upper()
288
+
289
+ # Build query string
290
+ params = kwargs.get("params")
291
+ if isinstance(params, dict) and params:
292
+ query_items = [f"{k}={v}" for k, v in params.items() if v is not None]
293
+ query_string = "&".join(query_items)
294
+ else:
295
+ query_string = url.query_string
296
+
297
+ # Build body string for signing and ensure sending matches signature
298
+ body = None
299
+ body_str = ""
300
+ if method_upper == "GET":
301
+ body_str = query_string or ""
302
+ else:
303
+ # Prefer original JSON object if present for deterministic signing
304
+ if kwargs.get("json") is not None:
305
+ body = kwargs.get("json")
306
+ else:
307
+ body = kwargs.get("data")
308
+
309
+ # If upstream already turned JSON into JsonPayload, extract its value
310
+ if isinstance(body, JsonPayload):
311
+ body_value = getattr(body, "_value", None)
312
+ else:
313
+ body_value = body
314
+
315
+ if isinstance(body_value, (dict, list)):
316
+ # Compact JSON to avoid whitespace discrepancies and sign exact bytes we send
317
+ body_str = pyjson.dumps(body_value, separators=(",", ":"), ensure_ascii=False)
318
+ kwargs["data"] = body_str
319
+ kwargs.pop("json", None)
320
+ elif isinstance(body_value, (str, bytes)):
321
+ # Sign and send exactly this string/bytes
322
+ body_str = body_value.decode("utf-8") if isinstance(body_value, bytes) else body_value
323
+ kwargs["data"] = body_str
324
+ kwargs.pop("json", None)
325
+ elif body_value is None:
326
+ body_str = ""
327
+ else:
328
+ # Fallback: string conversion (should still be JSON-like)
329
+ body_str = str(body_value)
330
+ kwargs["data"] = body_str
331
+ kwargs.pop("json", None)
332
+
333
+ # Prehash format: timestamp#memo#payload
334
+ message = f"{timestamp}#{memo}#{body_str}"
335
+ signature_hex = hmac.new(secret_bytes, message.encode("utf-8"), hashlib.sha256).hexdigest()
336
+
337
+ headers.update(
338
+ {
339
+ "X-BM-KEY": access_key,
340
+ "X-BM-TIMESTAMP": timestamp,
341
+ "X-BM-SIGN": signature_hex,
342
+ "Content-Type": "application/json; charset=UTF-8",
343
+ }
344
+ )
345
+
346
+ return args
347
+
252
348
  @staticmethod
253
349
  def coinw(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
254
350
  method: str = args[0]
@@ -348,4 +444,8 @@ pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
348
444
 
349
445
  pybotters.auth.Hosts.items["derivatives.bitmart.com"] = pybotters.auth.Item(
350
446
  "bitmart", Auth.bitmart
351
- )
447
+ )
448
+
449
+ pybotters.auth.Hosts.items["api-cloud-v2.bitmart.com"] = pybotters.auth.Item(
450
+ "bitmart_api", Auth.bitmart_api
451
+ )
@@ -10,6 +10,41 @@ import pybotters
10
10
 
11
11
  from .models.bitmart import BitmartDataStore
12
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
+
13
48
  class Bitmart:
14
49
  """Bitmart 合约交易(REST + WebSocket)。"""
15
50
 
@@ -24,17 +59,18 @@ class Bitmart:
24
59
  apis: str = None
25
60
  ) -> None:
26
61
  self.client = client
27
- self.store = BitmartDataStore()
62
+ self.store = BitmartDataStore2()
28
63
 
29
64
  self.public_api = public_api or "https://contract-v2.bitmart.com"
30
65
  self.private_api = "https://derivatives.bitmart.com"
31
66
  self.forward_api = f'{self.private_api}/gw-api/contract-tiger/forward'
32
67
  self.ws_url = ws_url or "wss://contract-ws-v2.bitmart.com/v1/ifcontract/realTime"
33
68
  self.api_ws_url = "wss://openapi-ws-v2.bitmart.com/api?protocol=1.1"
34
-
69
+ self.api_url = "https://api-cloud-v2.bitmart.com"
35
70
  self.account_index = account_index
36
71
  self.apis = apis
37
72
  self.symbol_to_contract_id: dict[str, str] = {}
73
+ self.book = Book()
38
74
 
39
75
  async def __aenter__(self) -> "Bitmart":
40
76
  await self.update("detail")
@@ -244,7 +280,6 @@ class Bitmart:
244
280
  symbol = entry.get("name") or entry.get("display_name")
245
281
  if contract_id is None or symbol is None:
246
282
  continue
247
- self.store.book.id_to_symbol[str(contract_id)] = str(symbol)
248
283
 
249
284
  if "orders" in results:
250
285
  resp = results["orders"]
@@ -280,69 +315,41 @@ class Bitmart:
280
315
  self,
281
316
  symbols: Sequence[str] | str,
282
317
  *,
283
- depth: str = "Depth",
284
318
  depth_limit: int | None = None,
285
- use_api_ws: bool = True,
286
319
  ) -> pybotters.ws.WebSocketApp:
287
320
  """Subscribe order book channel(s)."""
288
321
 
289
322
  if isinstance(symbols, str):
290
323
  symbols = [symbols]
324
+
291
325
 
292
326
  if not symbols:
293
327
  raise ValueError("symbols must not be empty")
294
328
  if depth_limit is not None:
295
329
  self.store.book.limit = depth_limit
296
- if not use_api_ws:
297
- missing = [sym for sym in symbols if self.get_contract_id(sym) is None]
298
- if missing:
299
- await self.update("detail")
300
- still_missing = [sym for sym in missing if self.get_contract_id(sym) is None]
301
- if still_missing:
302
- raise ValueError(f"Unknown symbols: {', '.join(still_missing)}")
303
-
304
-
305
-
306
- channels: list[str] = []
307
- for symbol in symbols:
308
- contract_id = self.get_contract_id(symbol)
309
- if contract_id is None:
310
- continue
311
- self.store.book.id_to_symbol[str(contract_id)] = symbol
312
- channels.append(f"{depth}:{contract_id}")
313
-
314
- if not channels:
315
- raise ValueError("No channels resolved for subscription")
316
-
317
- payload = {"action": "subscribe", "args": channels}
318
- # print(payload)
330
+
331
+ hdlr_json = self.store.book.on_message
319
332
 
320
- ws_app = self.client.ws_connect(
321
- self.api_ws_url,
322
- send_json=payload,
323
- hdlr_json=self.store.onmessage,
324
- )
325
- else:
326
- channels: list[str] = []
327
- for symbol in symbols:
328
- channels.append(f"futures/depthAll5:{symbol}@100ms")
333
+ channels: list[str] = []
334
+ for symbol in symbols:
335
+ channels.append(f"futures/depthAll5:{symbol}@100ms")
329
336
 
330
- if not channels:
331
- raise ValueError("No channels resolved for subscription")
337
+ if not channels:
338
+ raise ValueError("No channels resolved for subscription")
332
339
 
333
- payload = {"action": "subscribe", "args": channels}
334
- # print(payload)
340
+ payload = {"action": "subscribe", "args": channels}
335
341
 
336
- ws_app = self.client.ws_connect(
337
- self.api_ws_url,
338
- send_json=payload,
339
- hdlr_json=self.store.onmessage,
340
- autoping=False,
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
+ )
342
348
 
343
349
  await ws_app._event.wait()
344
350
  return ws_app
345
351
 
352
+
346
353
  def gen_order_id(self):
347
354
  ts = int(time.time() * 1000) # 13位毫秒时间戳
348
355
  rand = random.randint(100000, 999999) # 6位随机数
@@ -364,6 +371,7 @@ class Bitmart:
364
371
  trigger_price: float | None = None,
365
372
  custom_id: int | str | None = None,
366
373
  extra_params: dict[str, Any] | None = None,
374
+ use_api: bool = False,
367
375
  ) -> int:
368
376
  """Submit an order via ``submitOrder``.
369
377
  返回值: order_id (int)
@@ -462,42 +470,124 @@ class Bitmart:
462
470
  "open_type",
463
471
  )
464
472
 
465
- payload: dict[str, Any] = {
466
- "place_all_order": False,
467
- "contract_id": contract_id_int,
468
- "category": category,
469
- "price": price_fmt,
470
- "vol": contracts_int,
471
- "way": way_value,
472
- "mode": mode_value,
473
- "open_type": open_type_value,
474
- "leverage": leverage,
475
- "reverse_vol": reverse_vol,
476
- }
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
+ }
477
573
 
478
- if trigger_price is not None:
479
- payload["trigger_price"] = trigger_price
574
+ if trigger_price is not None:
575
+ payload["trigger_price"] = trigger_price
480
576
 
481
- payload["custom_id"] = custom_id or self.gen_order_id()
577
+ payload["custom_id"] = custom_id or self.gen_order_id()
482
578
 
483
- if extra_params:
484
- payload.update(extra_params)
485
-
486
- # print(payload)
487
- # exit()
488
-
489
- res = await self.client.post(
490
- f"{self.forward_api}/v1/ifcontract/submitOrder",
491
- json=payload,
492
- )
493
- resp = await res.json()
494
-
495
- if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
496
- raise ValueError(f"Bitmart submitOrder error: {resp}")
497
-
498
- # {"errno":"OK","message":"Success","data":{"order_id":3000236525013551},"success":true}
499
- # 直接取order_id返回
500
- return resp.get("data", {}).get("order_id")
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")
501
591
 
502
592
  async def cancel_order(
503
593
  self,