hyperquant 0.5__py3-none-any.whl → 0.7__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.
- hyperquant/broker/auth.py +144 -9
- hyperquant/broker/bitget.py +101 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/lbank.py +354 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/models/bitget.py +283 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/lbank.py +547 -0
- hyperquant/broker/models/ourbit.py +184 -135
- hyperquant/broker/ourbit.py +16 -5
- hyperquant/broker/ws.py +21 -3
- hyperquant/core.py +3 -0
- {hyperquant-0.5.dist-info → hyperquant-0.7.dist-info}/METADATA +1 -1
- hyperquant-0.7.dist-info/RECORD +29 -0
- hyperquant-0.5.dist-info/RECORD +0 -21
- {hyperquant-0.5.dist-info → hyperquant-0.7.dist-info}/WHEEL +0 -0
hyperquant/broker/auth.py
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
import base64
|
2
|
+
import hmac
|
3
|
+
import urllib.parse
|
1
4
|
import time
|
2
5
|
import hashlib
|
3
6
|
from typing import Any
|
4
|
-
from aiohttp import ClientWebSocketResponse
|
7
|
+
from aiohttp import ClientWebSocketResponse, FormData, JsonPayload
|
5
8
|
from multidict import CIMultiDict
|
6
9
|
from yarl import URL
|
7
10
|
import pybotters
|
@@ -13,8 +16,106 @@ def md5_hex(s: str) -> str:
|
|
13
16
|
return hashlib.md5(s.encode("utf-8")).hexdigest()
|
14
17
|
|
15
18
|
|
19
|
+
|
20
|
+
def serialize(obj, prefix=''):
|
21
|
+
"""
|
22
|
+
Python 版 UK/v:递归排序 + urlencode + 展平
|
23
|
+
"""
|
24
|
+
def _serialize(obj, prefix=''):
|
25
|
+
if obj is None:
|
26
|
+
return []
|
27
|
+
if isinstance(obj, dict):
|
28
|
+
items = []
|
29
|
+
for k in sorted(obj.keys()):
|
30
|
+
v = obj[k]
|
31
|
+
n = f"{prefix}[{k}]" if prefix else k
|
32
|
+
items.extend(_serialize(v, n))
|
33
|
+
return items
|
34
|
+
elif isinstance(obj, list):
|
35
|
+
# JS style: output key once, then join values by &
|
36
|
+
values = []
|
37
|
+
for v in obj:
|
38
|
+
if isinstance(v, dict):
|
39
|
+
# Recursively serialize dict, but drop key= part (just use value part)
|
40
|
+
sub = _serialize(v, prefix)
|
41
|
+
# sub is a list of key=value, but we want only value part
|
42
|
+
for s in sub:
|
43
|
+
# s is like 'key=value', need only value
|
44
|
+
parts = s.split('=', 1)
|
45
|
+
if len(parts) == 2:
|
46
|
+
values.append(parts[1])
|
47
|
+
else:
|
48
|
+
values.append(parts[0])
|
49
|
+
else:
|
50
|
+
# Handle booleans and empty strings
|
51
|
+
if isinstance(v, bool):
|
52
|
+
val = "true" if v else "false"
|
53
|
+
elif v == "":
|
54
|
+
val = ""
|
55
|
+
else:
|
56
|
+
val = str(v)
|
57
|
+
values.append(val)
|
58
|
+
return [f"{urllib.parse.quote(str(prefix))}={'&'.join(values)}"]
|
59
|
+
else:
|
60
|
+
# Handle booleans and empty strings
|
61
|
+
if isinstance(obj, bool):
|
62
|
+
val = "true" if obj else "false"
|
63
|
+
elif obj == "":
|
64
|
+
val = ""
|
65
|
+
else:
|
66
|
+
val = str(obj)
|
67
|
+
return [f"{urllib.parse.quote(str(prefix))}={val}"]
|
68
|
+
return "&".join(_serialize(obj, prefix))
|
69
|
+
|
16
70
|
# 🔑 Ourbit 的鉴权函数
|
17
71
|
class Auth:
|
72
|
+
@staticmethod
|
73
|
+
def edgex(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
74
|
+
method: str = args[0]
|
75
|
+
url: URL = args[1]
|
76
|
+
data = kwargs.get("data") or {}
|
77
|
+
headers: CIMultiDict = kwargs["headers"]
|
78
|
+
|
79
|
+
session = kwargs["session"]
|
80
|
+
api_key:str = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
|
81
|
+
secret = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][1]
|
82
|
+
passphrase:str = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][2]
|
83
|
+
passphrase = passphrase.split("-")[0]
|
84
|
+
timestamp = str(int(time.time() * 1000))
|
85
|
+
# timestamp = "1758535055061"
|
86
|
+
|
87
|
+
raw_body = ""
|
88
|
+
if data and method.upper() in ["POST", "PUT", "PATCH"] and data:
|
89
|
+
raw_body = serialize(data)
|
90
|
+
else:
|
91
|
+
raw_body = serialize(dict(url.query.items()))
|
92
|
+
|
93
|
+
|
94
|
+
secret_quoted = urllib.parse.quote(secret, safe="")
|
95
|
+
b64_secret = base64.b64encode(secret_quoted.encode("utf-8")).decode()
|
96
|
+
message = f"{timestamp}{method.upper()}{url.raw_path}{raw_body}"
|
97
|
+
sign = hmac.new(b64_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).hexdigest()
|
98
|
+
|
99
|
+
sigh_header = {
|
100
|
+
"X-edgeX-Api-Key": api_key,
|
101
|
+
"X-edgeX-Passphrase": passphrase,
|
102
|
+
"X-edgeX-Signature": sign,
|
103
|
+
"X-edgeX-Timestamp": timestamp,
|
104
|
+
}
|
105
|
+
# ws单独进行签名
|
106
|
+
if headers.get("Upgrade") == "websocket":
|
107
|
+
json_str = pyjson.dumps(sigh_header, separators=(",", ":"))
|
108
|
+
b64_str = base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
|
109
|
+
b64_str.replace("=", "")
|
110
|
+
headers.update({"Sec-WebSocket-Protocol": b64_str})
|
111
|
+
else:
|
112
|
+
headers.update(sigh_header)
|
113
|
+
|
114
|
+
if data:
|
115
|
+
kwargs.update({"data": JsonPayload(data)})
|
116
|
+
|
117
|
+
return args
|
118
|
+
|
18
119
|
@staticmethod
|
19
120
|
def ourbit(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
20
121
|
method: str = args[0]
|
@@ -67,18 +168,35 @@ class Auth:
|
|
67
168
|
token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
|
68
169
|
cookie = f"uc_token={token}; u_id={token}; "
|
69
170
|
headers.update({"cookie": cookie})
|
171
|
+
return args
|
70
172
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
# url = url.with_query(q)
|
173
|
+
@staticmethod
|
174
|
+
def lbank(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
175
|
+
method: str = args[0]
|
176
|
+
url: URL = args[1]
|
177
|
+
data = kwargs.get("data") or {}
|
178
|
+
headers: CIMultiDict = kwargs["headers"]
|
78
179
|
|
180
|
+
# 从 session 里取 api_key & secret
|
181
|
+
session = kwargs["session"]
|
182
|
+
token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
|
79
183
|
|
80
|
-
return args
|
81
184
|
|
185
|
+
# 设置 headers
|
186
|
+
headers.update(
|
187
|
+
{
|
188
|
+
"ex-language": 'zh-TW',
|
189
|
+
"ex-token": token,
|
190
|
+
"source": "4",
|
191
|
+
"versionflage": "true",
|
192
|
+
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
|
193
|
+
}
|
194
|
+
)
|
195
|
+
|
196
|
+
# 更新 kwargs.body,保证发出去的与签名一致
|
197
|
+
# kwargs.update({"data": raw_body_for_sign})
|
198
|
+
|
199
|
+
return args
|
82
200
|
|
83
201
|
pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
|
84
202
|
"ourbit", Auth.ourbit
|
@@ -86,3 +204,20 @@ pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
|
|
86
204
|
pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
|
87
205
|
"ourbit", Auth.ourbit_spot
|
88
206
|
)
|
207
|
+
|
208
|
+
pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
|
209
|
+
"ourbit", Auth.ourbit_spot
|
210
|
+
)
|
211
|
+
|
212
|
+
pybotters.auth.Hosts.items["pro.edgex.exchange"] = pybotters.auth.Item(
|
213
|
+
"edgex", Auth.edgex
|
214
|
+
)
|
215
|
+
|
216
|
+
|
217
|
+
pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
|
218
|
+
"edgex", Auth.edgex
|
219
|
+
)
|
220
|
+
|
221
|
+
pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
|
222
|
+
"lbank", Auth.lbank
|
223
|
+
)
|
@@ -0,0 +1,101 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import itertools
|
5
|
+
import logging
|
6
|
+
import time
|
7
|
+
from typing import Any, Iterable, Literal
|
8
|
+
|
9
|
+
import pybotters
|
10
|
+
|
11
|
+
from .models.bitget import BitgetDataStore
|
12
|
+
from .lib.util import fmt_value
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
class Bitget:
|
19
|
+
"""Bitget public market-data client (REST + WS)."""
|
20
|
+
|
21
|
+
def __init__(
|
22
|
+
self,
|
23
|
+
client: pybotters.Client,
|
24
|
+
*,
|
25
|
+
rest_api: str | None = None,
|
26
|
+
ws_url: str | None = None,
|
27
|
+
) -> None:
|
28
|
+
self.client = client
|
29
|
+
self.store = BitgetDataStore()
|
30
|
+
|
31
|
+
self.rest_api = rest_api or "https://api.bitget.com"
|
32
|
+
self.ws_url = ws_url or "wss://ws.bitget.com/v2/ws/public"
|
33
|
+
self.ws_url_private = ws_url or "wss://ws.bitget.com/v2/ws/private"
|
34
|
+
|
35
|
+
self._ws_app = None
|
36
|
+
|
37
|
+
|
38
|
+
async def __aenter__(self) -> "Bitget":
|
39
|
+
await self.update("detail")
|
40
|
+
return self
|
41
|
+
|
42
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
43
|
+
pass
|
44
|
+
|
45
|
+
async def update(
|
46
|
+
self,
|
47
|
+
update_type: Literal["detail", 'all'] = "all",
|
48
|
+
) -> None:
|
49
|
+
fet = []
|
50
|
+
if update_type in ("detail", "all"):
|
51
|
+
fet.append(
|
52
|
+
self.client.get(
|
53
|
+
f"{self.rest_api}/api/v2/mix/market/contracts?productType=usdt-futures",
|
54
|
+
)
|
55
|
+
)
|
56
|
+
|
57
|
+
await self.store.initialize(*fet)
|
58
|
+
|
59
|
+
async def place_order(
|
60
|
+
self,
|
61
|
+
symbol: str,
|
62
|
+
*,
|
63
|
+
direction: Literal["buy", "sell", "0", "1"],
|
64
|
+
volume: float,
|
65
|
+
price: float | None = None,
|
66
|
+
order_type: Literal["market", "limit_ioc", "limit_gtc"] = "market",
|
67
|
+
offset_flag: Literal["open", "close", "0", "1"] = "open",
|
68
|
+
exchange_id: str = "Exchange",
|
69
|
+
product_group: str = "SwapU",
|
70
|
+
order_proportion: str = "0.0000",
|
71
|
+
client_order_id: str | None = None,
|
72
|
+
) -> dict[str, Any]:
|
73
|
+
pass
|
74
|
+
|
75
|
+
async def cancel_order(
|
76
|
+
self,
|
77
|
+
order_sys_id: str,
|
78
|
+
*,
|
79
|
+
action_flag: str | int = "1",
|
80
|
+
) -> dict[str, Any]:
|
81
|
+
pass
|
82
|
+
|
83
|
+
|
84
|
+
async def sub_orderbook(self, symbols: list[str], channel: str = 'books1') -> None:
|
85
|
+
"""订阅指定交易对的订单簿(遵循 LBank 协议)。
|
86
|
+
"""
|
87
|
+
|
88
|
+
submsg = {
|
89
|
+
"op": "subscribe",
|
90
|
+
"args": []
|
91
|
+
}
|
92
|
+
for symbol in symbols:
|
93
|
+
submsg["args"].append(
|
94
|
+
{"instType": "SPOT", "channel": channel, "instId": symbol}
|
95
|
+
)
|
96
|
+
|
97
|
+
self.client.ws_connect(
|
98
|
+
self.ws_url,
|
99
|
+
send_json=submsg,
|
100
|
+
hdlr_json=self.store.onmessage
|
101
|
+
)
|