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.
- {hyperquant-1.21 → hyperquant-1.25}/PKG-INFO +1 -3
- {hyperquant-1.21 → hyperquant-1.25}/apis.json +5 -0
- {hyperquant-1.21 → hyperquant-1.25}/pyproject.toml +1 -3
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/auth.py +101 -1
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/bitmart.py +170 -80
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lighter.py +222 -12
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/bitmart.py +14 -25
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/lighter.py +360 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/ws.py +7 -0
- hyperquant-1.25/test.py +17 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_bitmart.py +98 -26
- hyperquant-1.25/tests/test_lighter.py +181 -0
- hyperquant-1.25/tests/test_store.py +182 -0
- {hyperquant-1.21 → hyperquant-1.25}/uv.lock +159 -220
- hyperquant-1.21/src/hyperquant/broker/coinup.py +0 -591
- hyperquant-1.21/src/hyperquant/broker/models/coinup.py +0 -334
- hyperquant-1.21/test.py +0 -48
- hyperquant-1.21/tests/test_lighter.py +0 -116
- {hyperquant-1.21 → hyperquant-1.25}/.gitignore +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/.python-version +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/README.md +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/data/alpine_smoke.log +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/data/logs/notikit.log +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/data/logs/test_order_sync.log +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/data/records_swap.csv +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/data/records_swapc.csv +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/doc/bitmart.md +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/doc/coinup.md +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/doc/lbank.md +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/pub.sh +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/requirements-dev.lock +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/requirements.lock +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/core.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/db.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/src/hyperquant/notikit.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_bitget.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_coinup.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_coinw.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_draw.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_edgex.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_lbank.py +0 -0
- {hyperquant-1.21 → hyperquant-1.25}/tests/test_ourbit.py +0 -0
- {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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
331
|
-
|
|
337
|
+
if not channels:
|
|
338
|
+
raise ValueError("No channels resolved for subscription")
|
|
332
339
|
|
|
333
|
-
|
|
334
|
-
# print(payload)
|
|
340
|
+
payload = {"action": "subscribe", "args": channels}
|
|
335
341
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
"
|
|
468
|
-
"
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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
|
-
|
|
479
|
-
|
|
574
|
+
if trigger_price is not None:
|
|
575
|
+
payload["trigger_price"] = trigger_price
|
|
480
576
|
|
|
481
|
-
|
|
577
|
+
payload["custom_id"] = custom_id or self.gen_order_id()
|
|
482
578
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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,
|