hyperquant 1.0__tar.gz → 1.24__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.0 → hyperquant-1.24}/PKG-INFO +2 -4
- hyperquant-1.24/apis.json +36 -0
- {hyperquant-1.0 → hyperquant-1.24}/pyproject.toml +2 -7
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/auth.py +101 -1
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/bitmart.py +265 -57
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/lighter.py +134 -2
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/bitmart.py +48 -4
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/lighter.py +301 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/ws.py +7 -0
- hyperquant-1.24/test.py +17 -0
- hyperquant-1.24/tests/test_bitmart.py +370 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_coinup.py +5 -5
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_lighter.py +65 -24
- hyperquant-1.24/tests/test_store.py +182 -0
- {hyperquant-1.0 → hyperquant-1.24}/uv.lock +76 -133
- hyperquant-1.0/apis.json +0 -31
- hyperquant-1.0/src/hyperquant/broker/coinup.py +0 -591
- hyperquant-1.0/src/hyperquant/broker/models/coinup.py +0 -334
- hyperquant-1.0/tests/test_bitmart.py +0 -156
- {hyperquant-1.0 → hyperquant-1.24}/.gitignore +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/.python-version +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/README.md +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/data/alpine_smoke.log +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/data/logs/notikit.log +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/data/logs/test_order_sync.log +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/data/records_swap.csv +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/data/records_swapc.csv +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/doc/bitmart.md +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/doc/coinup.md +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/doc/lbank.md +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/pub.sh +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/requirements-dev.lock +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/requirements.lock +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/__init__.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/bitget.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/coinw.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/lbank.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/bitget.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/coinw.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/lbank.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/core.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/db.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/draw.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/logkit.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/src/hyperquant/notikit.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_bitget.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_coinw.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_draw.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_edgex.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_lbank.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/tests/test_ourbit.py +0 -0
- {hyperquant-1.0 → hyperquant-1.24}/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.24
|
|
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
|
-
Requires-Dist: lighter-sdk
|
|
20
|
+
Requires-Dist: lighter-sdk>=0.1.4
|
|
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
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ourbit": [
|
|
3
|
+
"WEBa07743126a6cc69a896a501404ae8357e4116059ebc5bde75020881dc53a24ba"
|
|
4
|
+
],
|
|
5
|
+
"edgex": [
|
|
6
|
+
"a8b730c5-9039-8253-6cfa-8526c060beb3",
|
|
7
|
+
"78Uke-bPB57YoRzAwY7SkWyPQFeKkPuWVQakC0k9rSI",
|
|
8
|
+
"3MB7nAnnNPTxAnwdbjDPew-02b746d6a832346a46a97faf054b2909c1a0b58a35e04c3504923a99a5503c1c"
|
|
9
|
+
],
|
|
10
|
+
"lbank": [
|
|
11
|
+
"67782715959c4b15b5fadab8c1cec62d"
|
|
12
|
+
],
|
|
13
|
+
"bitget":[
|
|
14
|
+
"bg_03e0445d9282f248d22842cfe6f30192",
|
|
15
|
+
"67ec894753d75fec12332881278420863a960ec39c8f5acf1de88aa1da926854",
|
|
16
|
+
"huainian0408"
|
|
17
|
+
],
|
|
18
|
+
"coinw":[
|
|
19
|
+
"eedb6bc3-fc05-45b3-9bbc-53d2fd1b3a04",
|
|
20
|
+
"RGMYGU2XHXZNKRKGR6TFU1BZRKXYMOQKZUYY"
|
|
21
|
+
],
|
|
22
|
+
"lighter":[
|
|
23
|
+
"e16b22286c0d0d59b6d5db986791f43f747ae4323aac7aa4d64e62b9b3832fbb93af660904587877",
|
|
24
|
+
"41933ec0f5dace5eee47628ea2e2cb40a491f7fd20f983b385f8bdc5eea3d3ad4a47bd745f70fc2a"
|
|
25
|
+
],
|
|
26
|
+
"bitmart":[
|
|
27
|
+
"eyJoZWFkZXIiOnsidHlwIjoiQml0TWFydCIsImFsZyI6IkJNQVBJU0lYIn0sInBheWxvYWQiOnsiamlkIjoiMjk4Mjk1NWMzZTBlNDA5YmFjODYyZGNlOWY0MTFhNDkiLCJ2ZXJzaW9uIjoiMjAyNTExMDEiLCJleHBpcmVzQXQiOjE3NjIwNTE3MzI0NTMsImJtIjoiRVhFbjk2UzV2dGRISmtKZ0hYejlPcTA0a0ZTdFdHTWtRWG5yVHpDZktYdW1OOHFIVTh0WExUeXlTaEMzVFJDMlZHOVhuMWxkcVkzdVdlOHluWlBrTGhqQWNIT0VjK2VoeEhlc3VJVUh3U3ArRVFnMEFtY0JIWW53OVRuelRkb2Fob25jakxBNTdtRDVEK2tPSHNWUXdDdElXRGNsTWdpd1d3V3NlY0d5Y1hNdjJJd1pvUVAwZzRlZUtudTgwNDlpZ1RPdERkN0JLenlFN0tONy85ZWpBL1hHQmg4SzBROW9hTmN6aWwwMmxVUnBGNm9wUUJHVTY1eHhBTkMwR0Zyb2RZMmFNalRtQVN2Si9vdmJMNEQxZGtUcUp6azIxdVlhamllRm9OTmREdlZqMXlzOGdTTGJBc3FWMDlsd2x0M3lTWER2L093a0hyclhJMlAyZVZqU2JuK1hPODhKRTlFYWRpTkcwRU9EOWhLRy9YRFhoU0dVT1N5YnJrNWdLdjNoIn0sInNpZ25hdHVyZSI6InQ3SmRFZ3VhTzNrUU9RSlNBcVRqMDVQRCsrcGR6UjlLVDVZTGhNdmNQRDlvc0tWcjRYMEM0VjkwaStYTm0rMFBxNlhuZGo5SjFSeXFtZVBuVG5QUmRBPT0ifQ==",
|
|
28
|
+
"4OamVUwuSznwyStlmRIfc9o0y5tCYdMOh",
|
|
29
|
+
"1c0886dd192a1dd0f23f71f7ab577a45"
|
|
30
|
+
],
|
|
31
|
+
"bitmart_api":[
|
|
32
|
+
"21c0e5173835d02805b99641c807375866c41275",
|
|
33
|
+
"c82fbdb564f194362278b69370c47116db00165fc6a7f5ef770158fcb73a2c70",
|
|
34
|
+
"api"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "hyperquant"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.24"
|
|
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,9 +14,7 @@ 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
|
-
"
|
|
18
|
-
"rnet==3.0.0rc10",
|
|
19
|
-
"lighter-sdk",
|
|
17
|
+
"lighter-sdk>=0.1.4",
|
|
20
18
|
]
|
|
21
19
|
readme = "README.md"
|
|
22
20
|
requires-python = ">=3.11"
|
|
@@ -47,9 +45,6 @@ requires-python = ">=3.9"
|
|
|
47
45
|
[tool.hatch.build.targets.wheel]
|
|
48
46
|
packages = ["src/hyperquant"]
|
|
49
47
|
|
|
50
|
-
[tool.uv.sources]
|
|
51
|
-
lighter-sdk = { git = "https://github.com/elliottech/lighter-python.git" }
|
|
52
|
-
|
|
53
48
|
[dependency-groups]
|
|
54
49
|
dev = [
|
|
55
50
|
"twine>=6.1.0",
|
|
@@ -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,16 +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"
|
|
69
|
+
self.api_url = "https://api-cloud-v2.bitmart.com"
|
|
34
70
|
self.account_index = account_index
|
|
35
71
|
self.apis = apis
|
|
36
72
|
self.symbol_to_contract_id: dict[str, str] = {}
|
|
73
|
+
self.book = Book()
|
|
37
74
|
|
|
38
75
|
async def __aenter__(self) -> "Bitmart":
|
|
39
76
|
await self.update("detail")
|
|
@@ -243,7 +280,6 @@ class Bitmart:
|
|
|
243
280
|
symbol = entry.get("name") or entry.get("display_name")
|
|
244
281
|
if contract_id is None or symbol is None:
|
|
245
282
|
continue
|
|
246
|
-
self.store.book.id_to_symbol[str(contract_id)] = str(symbol)
|
|
247
283
|
|
|
248
284
|
if "orders" in results:
|
|
249
285
|
resp = results["orders"]
|
|
@@ -279,50 +315,41 @@ class Bitmart:
|
|
|
279
315
|
self,
|
|
280
316
|
symbols: Sequence[str] | str,
|
|
281
317
|
*,
|
|
282
|
-
depth: str = "Depth",
|
|
283
318
|
depth_limit: int | None = None,
|
|
284
319
|
) -> pybotters.ws.WebSocketApp:
|
|
285
320
|
"""Subscribe order book channel(s)."""
|
|
286
321
|
|
|
287
322
|
if isinstance(symbols, str):
|
|
288
323
|
symbols = [symbols]
|
|
324
|
+
|
|
289
325
|
|
|
290
326
|
if not symbols:
|
|
291
327
|
raise ValueError("symbols must not be empty")
|
|
292
|
-
|
|
293
|
-
missing = [sym for sym in symbols if self.get_contract_id(sym) is None]
|
|
294
|
-
if missing:
|
|
295
|
-
await self.update("detail")
|
|
296
|
-
still_missing = [sym for sym in missing if self.get_contract_id(sym) is None]
|
|
297
|
-
if still_missing:
|
|
298
|
-
raise ValueError(f"Unknown symbols: {', '.join(still_missing)}")
|
|
299
|
-
|
|
300
328
|
if depth_limit is not None:
|
|
301
329
|
self.store.book.limit = depth_limit
|
|
330
|
+
|
|
331
|
+
hdlr_json = self.store.book.on_message
|
|
302
332
|
|
|
303
333
|
channels: list[str] = []
|
|
304
334
|
for symbol in symbols:
|
|
305
|
-
|
|
306
|
-
if contract_id is None:
|
|
307
|
-
continue
|
|
308
|
-
self.store.book.id_to_symbol[str(contract_id)] = symbol
|
|
309
|
-
channels.append(f"{depth}:{contract_id}")
|
|
335
|
+
channels.append(f"futures/depthAll5:{symbol}@100ms")
|
|
310
336
|
|
|
311
337
|
if not channels:
|
|
312
338
|
raise ValueError("No channels resolved for subscription")
|
|
313
339
|
|
|
314
340
|
payload = {"action": "subscribe", "args": channels}
|
|
315
|
-
# print(payload)
|
|
316
341
|
|
|
317
342
|
ws_app = self.client.ws_connect(
|
|
318
|
-
self.
|
|
343
|
+
self.api_ws_url,
|
|
319
344
|
send_json=payload,
|
|
320
|
-
hdlr_json=
|
|
345
|
+
hdlr_json=hdlr_json,
|
|
321
346
|
autoping=False,
|
|
322
347
|
)
|
|
348
|
+
|
|
323
349
|
await ws_app._event.wait()
|
|
324
350
|
return ws_app
|
|
325
351
|
|
|
352
|
+
|
|
326
353
|
def gen_order_id(self):
|
|
327
354
|
ts = int(time.time() * 1000) # 13位毫秒时间戳
|
|
328
355
|
rand = random.randint(100000, 999999) # 6位随机数
|
|
@@ -344,6 +371,7 @@ class Bitmart:
|
|
|
344
371
|
trigger_price: float | None = None,
|
|
345
372
|
custom_id: int | str | None = None,
|
|
346
373
|
extra_params: dict[str, Any] | None = None,
|
|
374
|
+
use_api: bool = False,
|
|
347
375
|
) -> int:
|
|
348
376
|
"""Submit an order via ``submitOrder``.
|
|
349
377
|
返回值: order_id (int)
|
|
@@ -363,7 +391,7 @@ class Bitmart:
|
|
|
363
391
|
if detail is None:
|
|
364
392
|
raise ValueError(f"Market metadata unavailable for symbol: {symbol}")
|
|
365
393
|
|
|
366
|
-
if
|
|
394
|
+
if qty is not None:
|
|
367
395
|
|
|
368
396
|
contract_size_str = detail.get("contract_size") or detail.get("vol_unit") or "1"
|
|
369
397
|
try:
|
|
@@ -380,10 +408,10 @@ class Bitmart:
|
|
|
380
408
|
f"Volume too small for contract size ({contract_size_val}): volume={qty}"
|
|
381
409
|
)
|
|
382
410
|
|
|
383
|
-
|
|
384
|
-
contracts_int = int(
|
|
411
|
+
if qty_contract is not None:
|
|
412
|
+
contracts_int = int(qty_contract)
|
|
385
413
|
if contracts_int <= 0:
|
|
386
|
-
raise ValueError(f"Volume must be positive integer contracts: volume={
|
|
414
|
+
raise ValueError(f"Volume must be positive integer contracts: volume={qty_contract}")
|
|
387
415
|
|
|
388
416
|
price_unit = detail.get("price_unit") or 1
|
|
389
417
|
try:
|
|
@@ -442,42 +470,124 @@ class Bitmart:
|
|
|
442
470
|
"open_type",
|
|
443
471
|
)
|
|
444
472
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
"
|
|
448
|
-
"
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
+
}
|
|
457
573
|
|
|
458
|
-
|
|
459
|
-
|
|
574
|
+
if trigger_price is not None:
|
|
575
|
+
payload["trigger_price"] = trigger_price
|
|
460
576
|
|
|
461
|
-
|
|
577
|
+
payload["custom_id"] = custom_id or self.gen_order_id()
|
|
462
578
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
476
|
-
raise ValueError(f"Bitmart submitOrder error: {resp}")
|
|
477
|
-
|
|
478
|
-
# {"errno":"OK","message":"Success","data":{"order_id":3000236525013551},"success":true}
|
|
479
|
-
# 直接取order_id返回
|
|
480
|
-
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")
|
|
481
591
|
|
|
482
592
|
async def cancel_order(
|
|
483
593
|
self,
|
|
@@ -510,3 +620,101 @@ class Bitmart:
|
|
|
510
620
|
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
511
621
|
raise ValueError(f"Bitmart cancelOrders error: {resp}")
|
|
512
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
|