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 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
- # wss消息增加参数
72
- # if headers.get("Upgrade") == "websocket":
73
- # args = (method, url)
74
- # # 拼接 token
75
- # q = dict(url.query)
76
- # q["token"] = token
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
+ )