hyperquant 1.21__py3-none-any.whl → 1.22__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.
- hyperquant/broker/auth.py +101 -1
- hyperquant/broker/bitmart.py +170 -80
- hyperquant/broker/models/bitmart.py +14 -25
- {hyperquant-1.21.dist-info → hyperquant-1.22.dist-info}/METADATA +2 -1
- {hyperquant-1.21.dist-info → hyperquant-1.22.dist-info}/RECORD +6 -6
- {hyperquant-1.21.dist-info → hyperquant-1.22.dist-info}/WHEEL +0 -0
hyperquant/broker/auth.py
CHANGED
|
@@ -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
|
+
)
|
hyperquant/broker/bitmart.py
CHANGED
|
@@ -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,
|
|
@@ -98,29 +98,11 @@ class Book(DataStore):
|
|
|
98
98
|
self._last_update = ms_t
|
|
99
99
|
|
|
100
100
|
def _on_message_api(self, msg: dict[str, Any]) -> None:
|
|
101
|
-
# {
|
|
102
|
-
# "data": {
|
|
103
|
-
# "symbol": "BTCUSDT",
|
|
104
|
-
# "asks": [
|
|
105
|
-
# {
|
|
106
|
-
# "price": "70294.4",
|
|
107
|
-
# "vol": "455"
|
|
108
|
-
# }
|
|
109
|
-
# ],
|
|
110
|
-
# "bids": [
|
|
111
|
-
# {
|
|
112
|
-
# "price": "70293.9",
|
|
113
|
-
# "vol": "1856"
|
|
114
|
-
# }
|
|
115
|
-
# ],
|
|
116
|
-
# "ms_t": 1730399750402
|
|
117
|
-
# },
|
|
118
|
-
# "group": "futures/depthAll20:BTCUSDT@200ms"
|
|
119
|
-
# }
|
|
120
101
|
data = msg.get("data")
|
|
121
102
|
if not isinstance(data, dict):
|
|
122
103
|
return
|
|
123
|
-
symbol
|
|
104
|
+
# Some callers embed symbol at top-level; prefer msg["symbol"] when present
|
|
105
|
+
symbol = msg.get("symbol") or data.get("symbol")
|
|
124
106
|
asks = data.get("asks") or []
|
|
125
107
|
bids = data.get("bids") or []
|
|
126
108
|
if self.limit:
|
|
@@ -128,12 +110,19 @@ class Book(DataStore):
|
|
|
128
110
|
bids = bids[: self.limit]
|
|
129
111
|
|
|
130
112
|
self._find_and_delete({'s': symbol})
|
|
113
|
+
# OpenAPI order book arrays are typically [price, size, timestamp]
|
|
114
|
+
def _normalize_level(level: Any) -> tuple[str, str]:
|
|
115
|
+
if isinstance(level, dict):
|
|
116
|
+
return str(level.get("price", "0")), str(level.get("vol", "0"))
|
|
117
|
+
if isinstance(level, (list, tuple)) and len(level) >= 2:
|
|
118
|
+
return str(level[0]), str(level[1])
|
|
119
|
+
return "0", "0"
|
|
120
|
+
|
|
131
121
|
self._update([
|
|
132
122
|
self._make_entry(
|
|
133
123
|
symbol,
|
|
134
124
|
"a",
|
|
135
|
-
entry
|
|
136
|
-
entry.get("vol", '0'),
|
|
125
|
+
*_normalize_level(entry),
|
|
137
126
|
)
|
|
138
127
|
for entry in asks
|
|
139
128
|
])
|
|
@@ -141,11 +130,11 @@ class Book(DataStore):
|
|
|
141
130
|
self._make_entry(
|
|
142
131
|
symbol,
|
|
143
132
|
"b",
|
|
144
|
-
entry
|
|
145
|
-
entry.get("vol", '0'),
|
|
133
|
+
*_normalize_level(entry),
|
|
146
134
|
)
|
|
147
135
|
for entry in bids
|
|
148
136
|
])
|
|
137
|
+
|
|
149
138
|
|
|
150
139
|
def sorted(self, query: Item | None = None, limit: int | None = None) -> dict[str, list[Item]]:
|
|
151
140
|
return self._sorted(
|
|
@@ -325,7 +314,7 @@ class BitmartDataStore(DataStoreCollection):
|
|
|
325
314
|
self._create("positions", datastore_class=Positions)
|
|
326
315
|
self._create("balances", datastore_class=Balances)
|
|
327
316
|
self._create("ticker", datastore_class=Ticker)
|
|
328
|
-
|
|
317
|
+
|
|
329
318
|
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
330
319
|
if isinstance(msg, dict):
|
|
331
320
|
group = msg.get("group")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperquant
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.22
|
|
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
|
|
@@ -19,6 +19,7 @@ Requires-Dist: cryptography>=44.0.2
|
|
|
19
19
|
Requires-Dist: curl-cffi>=0.13.0
|
|
20
20
|
Requires-Dist: duckdb>=1.2.2
|
|
21
21
|
Requires-Dist: lighter-sdk
|
|
22
|
+
Requires-Dist: numba>=0.62.1
|
|
22
23
|
Requires-Dist: numpy>=1.21.0
|
|
23
24
|
Requires-Dist: pandas>=2.2.3
|
|
24
25
|
Requires-Dist: pybotters>=1.9.1
|
|
@@ -4,9 +4,9 @@ hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
|
|
|
4
4
|
hyperquant/draw.py,sha256=up_lQ3pHeVLoNOyh9vPjgNwjD0M-6_IetSGviQUgjhY,54624
|
|
5
5
|
hyperquant/logkit.py,sha256=nUo7nx5eONvK39GOhWwS41zNRL756P2J7-5xGzwXnTY,8462
|
|
6
6
|
hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
|
|
7
|
-
hyperquant/broker/auth.py,sha256=
|
|
7
|
+
hyperquant/broker/auth.py,sha256=3Ws_2iaF2NTCVIbkLzL_0ae3y7f4vgj9zmozvGaUXnk,16029
|
|
8
8
|
hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
|
|
9
|
-
hyperquant/broker/bitmart.py,sha256=
|
|
9
|
+
hyperquant/broker/bitmart.py,sha256=7j_8TU3Dxjj5HCNX7CbSO3nPZcQH1t31A9UOv5tTbg0,25974
|
|
10
10
|
hyperquant/broker/coinup.py,sha256=eOr8BTRXiTb5tCU2FDmvBdXXgqiwVmCbP5pdeA1ORJ8,20390
|
|
11
11
|
hyperquant/broker/coinw.py,sha256=SnJU0vASh77rfcpMGWaIfTblQSjQk3vjlW_4juYdbcs,17214
|
|
12
12
|
hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
|
|
@@ -20,7 +20,7 @@ hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb8
|
|
|
20
20
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
|
21
21
|
hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
|
|
22
22
|
hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
|
|
23
|
-
hyperquant/broker/models/bitmart.py,sha256=
|
|
23
|
+
hyperquant/broker/models/bitmart.py,sha256=O9RnU-XBeR9SzicG15jzuzK5oy2kMrRJAyZSqC8DXUw,21938
|
|
24
24
|
hyperquant/broker/models/coinup.py,sha256=X_ngB2_sgTOdfAZqTyeWvCN03j-0_inZ6ugZKW6hR7k,11173
|
|
25
25
|
hyperquant/broker/models/coinw.py,sha256=LvLMVP7i-qkkTK1ubw8eBkMK2RQmFoKPxdKqmC4IToY,22157
|
|
26
26
|
hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
|
|
@@ -32,6 +32,6 @@ hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw
|
|
|
32
32
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
|
33
33
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
|
34
34
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
|
35
|
-
hyperquant-1.
|
|
36
|
-
hyperquant-1.
|
|
37
|
-
hyperquant-1.
|
|
35
|
+
hyperquant-1.22.dist-info/METADATA,sha256=dH0APcdAjkH7pyeQiyPyTd3tl1E2tQVYxMd5YlbzjrE,4438
|
|
36
|
+
hyperquant-1.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
37
|
+
hyperquant-1.22.dist-info/RECORD,,
|
|
File without changes
|