hyperquant 0.81__py3-none-any.whl → 0.83__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 CHANGED
@@ -192,13 +192,81 @@ class Auth:
192
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
193
  }
194
194
  )
195
- print(headers)
196
195
 
197
196
  # 更新 kwargs.body,保证发出去的与签名一致
198
197
  # kwargs.update({"data": raw_body_for_sign})
199
198
 
200
199
  return args
201
200
 
201
+ @staticmethod
202
+ def coinw(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
203
+ method: str = args[0]
204
+ url: URL = args[1]
205
+ headers: CIMultiDict = kwargs["headers"]
206
+
207
+ session = kwargs["session"]
208
+ try:
209
+ api_key, secret, _ = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name]
210
+ except (KeyError, ValueError):
211
+ raise RuntimeError("CoinW credentials (api_key, secret) are required")
212
+
213
+ timestamp = str(int(time.time() * 1000))
214
+ method_upper = method.upper()
215
+
216
+ params = kwargs.get("params")
217
+ query_string = ""
218
+ if isinstance(params, dict) and params:
219
+ query_items = [
220
+ f"{key}={value}"
221
+ for key, value in params.items()
222
+ if value is not None
223
+ ]
224
+ query_string = "&".join(query_items)
225
+ elif url.query_string:
226
+ query_string = url.query_string
227
+
228
+ body_str = ""
229
+
230
+ if method_upper == "GET":
231
+ body = None
232
+ data = None
233
+ else:
234
+ body = kwargs.get("json")
235
+ data = kwargs.get("data")
236
+ payload = body if body is not None else data
237
+ if isinstance(payload, (dict, list)):
238
+ body_str = pyjson.dumps(payload, separators=(",", ":"), ensure_ascii=False)
239
+ kwargs["data"] = body_str
240
+ kwargs.pop("json", None)
241
+ elif payload is not None:
242
+ body_str = str(payload)
243
+ kwargs["data"] = body_str
244
+ kwargs.pop("json", None)
245
+
246
+ if query_string:
247
+ path = f"{url.raw_path}?{query_string}"
248
+ else:
249
+ path = url.raw_path
250
+
251
+ message = f"{timestamp}{method_upper}{path}{body_str}"
252
+ signature = hmac.new(
253
+ secret, message.encode("utf-8"), hashlib.sha256
254
+ ).digest()
255
+ signature_b64 = base64.b64encode(signature).decode("ascii")
256
+
257
+ headers.update(
258
+ {
259
+ "sign": signature_b64,
260
+ "api_key": api_key,
261
+ "timestamp": timestamp,
262
+ }
263
+ )
264
+
265
+ if method_upper in {"POST", "PUT", "PATCH", "DELETE"} and "data" in kwargs:
266
+ headers.setdefault("Content-Type", "application/json")
267
+
268
+ return args
269
+
202
270
  pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
203
271
  "ourbit", Auth.ourbit
204
272
  )
@@ -221,4 +289,8 @@ pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
221
289
 
222
290
  pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
223
291
  "lbank", Auth.lbank
224
- )
292
+ )
293
+
294
+ pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
295
+ "coinw", Auth.coinw
296
+ )
@@ -0,0 +1,405 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any, Literal, Sequence
6
+
7
+ import pybotters
8
+
9
+ from .models.coinw import CoinwFuturesDataStore
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class Coinw:
15
+ """CoinW 永续合约客户端(REST + WebSocket)。"""
16
+
17
+ def __init__(
18
+ self,
19
+ client: pybotters.Client,
20
+ *,
21
+ rest_api: str | None = None,
22
+ ws_url: str | None = None,
23
+ web_api: str | None = None,
24
+ ) -> None:
25
+ self.client = client
26
+ self.store = CoinwFuturesDataStore()
27
+
28
+ self.rest_api = rest_api or "https://api.coinw.com"
29
+ self.ws_url_public = ws_url or "wss://ws.futurescw.com/perpum"
30
+ self.ws_url_private = self.ws_url_public
31
+ self.web_api = web_api or "https://futuresapi.coinw.com"
32
+
33
+ self._ws_private: pybotters.ws.WebSocketApp | None = None
34
+ self._ws_private_ready = asyncio.Event()
35
+ self._ws_headers = {
36
+ "Origin": "https://www.coinw.com",
37
+ "Referer": "https://www.coinw.com/",
38
+ "User-Agent": (
39
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
40
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
41
+ ),
42
+ }
43
+
44
+ async def __aenter__(self) -> "Coinw":
45
+ await self.update("detail")
46
+ return self
47
+
48
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
49
+ return None
50
+
51
+ async def update(
52
+ self,
53
+ update_type: Literal[
54
+ "detail",
55
+ "orders",
56
+ "position",
57
+ "balance",
58
+ "all",
59
+ ] = "all",
60
+ *,
61
+ instrument: str | None = None,
62
+ position_type: Literal["execute", "plan", "planTrigger"] = "execute",
63
+ page: int | None = None,
64
+ page_size: int | None = None,
65
+ open_ids: str | None = None,
66
+ ) -> None:
67
+ """刷新本地缓存,使用 CoinW REST API。
68
+
69
+ - detail: ``GET /v1/perpum/instruments`` (公共)
70
+ - orders: ``GET /v1/perpum/orders/open`` (私有,需要 ``instrument``)
71
+ - position: ``GET /v1/perpum/positions`` (私有,需要 ``instrument``)
72
+ - balance: ``GET /v1/perpum/account/getUserAssets`` (私有)
73
+ """
74
+
75
+ requests: list[Any] = []
76
+
77
+ include_detail = update_type in {"detail", "all"}
78
+ include_orders = update_type in {"orders", "all"}
79
+ include_position = update_type in {"position", "all"}
80
+ include_balance = update_type in {"balance", "all"}
81
+
82
+ if include_detail:
83
+ requests.append(self.client.get(f"{self.rest_api}/v1/perpum/instruments"))
84
+
85
+ if include_orders:
86
+ if not instrument:
87
+ raise ValueError("instrument is required when updating orders")
88
+ params: dict[str, Any] = {
89
+ "instrument": instrument,
90
+ "positionType": position_type,
91
+ }
92
+ if page is not None:
93
+ params["page"] = page
94
+ if page_size is not None:
95
+ params["pageSize"] = page_size
96
+ requests.append(
97
+ self.client.get(
98
+ f"{self.rest_api}/v1/perpum/orders/open",
99
+ params=params,
100
+ )
101
+ )
102
+
103
+ if include_position:
104
+ if not instrument:
105
+ raise ValueError("instrument is required when updating positions")
106
+ params = {"instrument": instrument}
107
+ if open_ids:
108
+ params["openIds"] = open_ids
109
+ requests.append(
110
+ self.client.get(
111
+ f"{self.rest_api}/v1/perpum/positions",
112
+ params=params,
113
+ )
114
+ )
115
+
116
+ if include_balance:
117
+ requests.append(
118
+ self.client.get(f"{self.rest_api}/v1/perpum/account/getUserAssets")
119
+ )
120
+
121
+ if not requests:
122
+ raise ValueError(f"update_type err: {update_type}")
123
+
124
+ await self.store.initialize(*requests)
125
+
126
+ async def place_order(
127
+ self,
128
+ instrument: str,
129
+ *,
130
+ direction: Literal["long", "short"],
131
+ leverage: int,
132
+ quantity: float | str,
133
+ quantity_unit: Literal[0, 1, 2, "quote", "contract", "base"] = 0,
134
+ position_model: Literal[0, 1, "isolated", "cross"] = 0,
135
+ position_type: Literal["execute", "plan", "planTrigger"] = "execute",
136
+ price: float | None = None,
137
+ trigger_price: float | None = None,
138
+ trigger_type: Literal[0, 1] | None = None,
139
+ stop_loss_price: float | None = None,
140
+ stop_profit_price: float | None = None,
141
+ third_order_id: str | None = None,
142
+ use_almighty_gold: bool | None = None,
143
+ gold_id: int | None = None,
144
+ ) -> dict[str, Any]:
145
+ """``POST /v1/perpum/order`` 下单。"""
146
+
147
+ payload: dict[str, Any] = {
148
+ "instrument": instrument,
149
+ "direction": self._normalize_direction(direction),
150
+ "leverage": int(leverage),
151
+ "quantityUnit": self._normalize_quantity_unit(quantity_unit),
152
+ "quantity": self._format_quantity(quantity),
153
+ "positionModel": self._normalize_position_model(position_model),
154
+ "positionType": position_type,
155
+ }
156
+
157
+ if price is not None:
158
+ payload["openPrice"] = price
159
+ if trigger_price is not None:
160
+ payload["triggerPrice"] = trigger_price
161
+ if trigger_type is not None:
162
+ payload["triggerType"] = int(trigger_type)
163
+ if stop_loss_price is not None:
164
+ payload["stopLossPrice"] = stop_loss_price
165
+ if stop_profit_price is not None:
166
+ payload["stopProfitPrice"] = stop_profit_price
167
+ if third_order_id:
168
+ payload["thirdOrderId"] = third_order_id
169
+ if use_almighty_gold is not None:
170
+ payload["useAlmightyGold"] = int(bool(use_almighty_gold))
171
+ if gold_id is not None:
172
+ payload["goldId"] = int(gold_id)
173
+
174
+ res = await self.client.post(
175
+ f"{self.rest_api}/v1/perpum/order",
176
+ data=payload,
177
+ )
178
+
179
+ data = await res.json()
180
+ return self._ensure_ok("place_order", data)
181
+
182
+ async def place_order_web(
183
+ self,
184
+ instrument: str,
185
+ *,
186
+ direction: Literal["long", "short"],
187
+ leverage: int | str,
188
+ quantity_unit: Literal[0, 1, 2],
189
+ quantity: str | float | int,
190
+ position_model: Literal[0, 1] = 1,
191
+ position_type: Literal["plan", "planTrigger", "execute"] = 'plan',
192
+ open_price: str | float | None = None,
193
+ contract_type: int = 1,
194
+ data_type: str = "trade_take",
195
+ device_id: str,
196
+ token: str,
197
+ headers: dict[str, str] | None = None,
198
+ ) -> dict[str, Any]:
199
+ """使用 Web 前端接口下单,绕过部分 API 频控策略。
200
+
201
+ 注意此接口需要传入真实浏览器参数,如 ``device_id`` 与 ``token``。
202
+ """
203
+
204
+ if not device_id or not token:
205
+ raise ValueError("device_id and token are required for place_order_web")
206
+
207
+ url = f"{self.web_api}/v1/futuresc/thirdClient/trade/{instrument}/open"
208
+
209
+ payload: dict[str, Any] = {
210
+ "instrument": instrument,
211
+ "direction": direction,
212
+ "leverage": str(leverage),
213
+ "quantityUnit": quantity_unit,
214
+ "quantity": str(quantity),
215
+ "positionModel": position_model,
216
+ "positionType": position_type,
217
+ "contractType": contract_type,
218
+ "dataType": data_type,
219
+ }
220
+ if open_price is not None:
221
+ payload["openPrice"] = str(open_price)
222
+
223
+ base_headers = {
224
+ "accept": "application/json, text/plain, */*",
225
+ "accept-language": "zh_CN",
226
+ "appversion": "100.100.100",
227
+ "cache-control": "no-cache",
228
+ "clienttag": "web",
229
+ "content-type": "application/json",
230
+ "cwdeviceid": device_id,
231
+ "deviceid": device_id,
232
+ "devicename": "Chrome V141.0.0.0 (macOS)",
233
+ "language": "zh_CN",
234
+ "logintoken": token,
235
+ "origin": "https://www.coinw.com",
236
+ "pragma": "no-cache",
237
+ "priority": "u=1, i",
238
+ "referer": "https://www.coinw.com/",
239
+ "sec-ch-ua": '"Microsoft Edge";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
240
+ "sec-ch-ua-mobile": "?0",
241
+ "sec-ch-ua-platform": '"macOS"',
242
+ "sec-fetch-dest": "empty",
243
+ "sec-fetch-mode": "cors",
244
+ "sec-fetch-site": "same-site",
245
+ "selecttype": "USD",
246
+ "systemversion": "macOS 10.15.7",
247
+ "thirdappid": "coinw",
248
+ "thirdapptoken": token,
249
+ "token": token,
250
+ "user-agent": (
251
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
252
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 "
253
+ "Safari/537.36 Edg/141.0.0.0"
254
+ ),
255
+ "withcredentials": "true",
256
+ "x-authorization": token,
257
+ "x-language": "zh_CN",
258
+ "x-locale": "zh_CN",
259
+ "x-requested-with": "XMLHttpRequest",
260
+ }
261
+ if headers:
262
+ base_headers.update(headers)
263
+
264
+ res = await self.client.post(
265
+ url,
266
+ json=payload,
267
+ headers=base_headers,
268
+ auth=None,
269
+ )
270
+ return await res.json()
271
+
272
+ async def cancel_order(self, order_id: str | int) -> dict[str, Any]:
273
+ """``DELETE /v1/perpum/order`` 取消单个订单。"""
274
+
275
+ res = await self.client.delete(
276
+ f"{self.rest_api}/v1/perpum/order",
277
+ data={"id": str(order_id)},
278
+ )
279
+ data = await res.json()
280
+ return self._ensure_ok("cancel_order", data)
281
+
282
+ async def sub_personal(self) -> None:
283
+ """订阅订单、持仓、资产私有频道。"""
284
+
285
+ ws_app = await self._ensure_private_ws()
286
+ payloads = [
287
+ {"event": "sub", "params": {"biz": "futures", "type": "order"}},
288
+ {"event": "sub", "params": {"biz": "futures", "type": "position"}},
289
+ {"event": "sub", "params": {"biz": "futures", "type": "position_change"}},
290
+ {"event": "sub", "params": {"biz": "futures", "type": "assets"}},
291
+ ]
292
+ for payload in payloads:
293
+ if ws_app.current_ws.closed:
294
+ raise ConnectionError("CoinW private websocket closed before subscription.")
295
+ await ws_app.current_ws.send_json(payload)
296
+ await asyncio.sleep(0.05)
297
+
298
+ async def sub_orderbook(
299
+ self,
300
+ pair_codes: Sequence[str] | str,
301
+ *,
302
+ depth_limit: int | None = None,
303
+ biz: str = "futures",
304
+ ) -> pybotters.ws.WebSocketApp:
305
+ """订阅 ``type=depth`` 订单簿数据,批量控制发送频率。"""
306
+
307
+ if isinstance(pair_codes, str):
308
+ pair_codes = [pair_codes]
309
+
310
+ pair_list = [code for code in pair_codes if code]
311
+ if not pair_list:
312
+ raise ValueError("pair_codes must not be empty")
313
+
314
+ self.store.book.limit = depth_limit
315
+
316
+ subscriptions = [
317
+ {"event": "sub", "params": {"biz": biz, "type": "depth", "pairCode": code}}
318
+ for code in pair_list
319
+ ]
320
+
321
+ ws_app = self.client.ws_connect(
322
+ self.ws_url_public,
323
+ hdlr_json=self.store.onmessage,
324
+ headers=self._ws_headers,
325
+ )
326
+ await ws_app._event.wait()
327
+
328
+ chunk_size = 10
329
+ for idx in range(0, len(subscriptions), chunk_size):
330
+ batch = subscriptions[idx : idx + chunk_size]
331
+ for msg in batch:
332
+ await ws_app.current_ws.send_json(msg)
333
+ if idx + chunk_size < len(subscriptions):
334
+ await asyncio.sleep(2.05)
335
+
336
+ return ws_app
337
+
338
+ async def _ensure_private_ws(self) -> pybotters.ws.WebSocketApp:
339
+
340
+ ws_app = self.client.ws_connect(
341
+ self.ws_url_private,
342
+ hdlr_json=self.store.onmessage,
343
+ headers=self._ws_headers,
344
+ )
345
+ await ws_app._event.wait()
346
+ await ws_app.current_ws._wait_authtask()
347
+
348
+ return ws_app
349
+
350
+ @staticmethod
351
+ def _normalize_direction(direction: str) -> str:
352
+ allowed = {"long", "short"}
353
+ value = str(direction).lower()
354
+ if value not in allowed:
355
+ raise ValueError(f"Unsupported direction: {direction}")
356
+ return value
357
+
358
+ @staticmethod
359
+ def _normalize_quantity_unit(
360
+ unit: Literal[0, 1, 2, "quote", "contract", "base"],
361
+ ) -> int:
362
+ mapping = {
363
+ 0: 0,
364
+ 1: 1,
365
+ 2: 2,
366
+ "quote": 0,
367
+ "contract": 1,
368
+ "base": 2,
369
+ }
370
+ try:
371
+ return mapping[unit] # type: ignore[index]
372
+ except KeyError as exc: # pragma: no cover - guard
373
+ raise ValueError(f"Unsupported quantity_unit: {unit}") from exc
374
+
375
+ @staticmethod
376
+ def _normalize_position_model(
377
+ model: Literal[0, 1, "isolated", "cross"],
378
+ ) -> int:
379
+ mapping = {
380
+ 0: 0,
381
+ 1: 1,
382
+ "isolated": 0,
383
+ "cross": 1,
384
+ }
385
+ try:
386
+ return mapping[model] # type: ignore[index]
387
+ except KeyError as exc: # pragma: no cover - guard
388
+ raise ValueError(f"Unsupported position_model: {model}") from exc
389
+
390
+ @staticmethod
391
+ def _format_quantity(quantity: float | str) -> str:
392
+ if isinstance(quantity, str):
393
+ return quantity
394
+ return str(quantity)
395
+
396
+ @staticmethod
397
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
398
+ """CoinW REST 成功时返回 ``{'code': 0, ...}``。"""
399
+
400
+ if not isinstance(data, dict) or data.get("code") != 0:
401
+ raise RuntimeError(f"{operation} failed: {data}")
402
+ payload = data.get("data")
403
+ if isinstance(payload, dict):
404
+ return payload
405
+ return {"data": payload}
@@ -567,7 +567,6 @@ class Lbank:
567
567
  headers=self._rest_headers,
568
568
  )
569
569
  data = await res.json()
570
- print(data)
571
570
  return self._ensure_ok("query_all", data)
572
571
 
573
572
 
@@ -0,0 +1,612 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import TYPE_CHECKING, Any, Awaitable
5
+
6
+ import aiohttp
7
+ from pybotters.store import DataStore, DataStoreCollection
8
+
9
+ if TYPE_CHECKING:
10
+ from pybotters.typedefs import Item
11
+ from pybotters.ws import ClientWebSocketResponse
12
+
13
+
14
+ class Book(DataStore):
15
+ """CoinW 合约订单簿数据存储。
16
+
17
+ WebSocket 频道: futures/depth
18
+
19
+ 消息示例(来源: https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
20
+
21
+ .. code:: json
22
+
23
+ {
24
+ "biz": "futures",
25
+ "pairCode": "BTC",
26
+ "type": "depth",
27
+ "data": {
28
+ "asks": [{"p": "95640.3", "m": "0.807"}, ...],
29
+ "bids": [{"p": "95640.2", "m": "0.068"}, ...]
30
+ }
31
+ }
32
+ """
33
+
34
+ _KEYS = ["s", "S", "p", "q"]
35
+
36
+ def _init(self) -> None:
37
+ self.limit: int | None = None
38
+
39
+ def _on_message(self, msg: dict[str, Any]) -> None:
40
+ data = msg.get("data")
41
+ if not isinstance(data, dict):
42
+ return
43
+
44
+ asks = data.get("asks") or []
45
+ bids = data.get("bids") or []
46
+ if not asks and not bids:
47
+ return
48
+
49
+ symbol = (
50
+ msg.get("pairCode")
51
+ or data.get("pairCode")
52
+ or msg.get("symbol")
53
+ or data.get("symbol")
54
+ )
55
+ if not symbol:
56
+ return
57
+
58
+ if self.limit is not None:
59
+ asks = asks[: self.limit]
60
+ bids = bids[: self.limit]
61
+
62
+ entries: list[dict[str, Any]] = []
63
+ for side, levels in (("a", asks), ("b", bids)):
64
+ for level in levels:
65
+ price = level.get("p") or level.get("price")
66
+ size = level.get("m") or level.get("q") or level.get("size")
67
+ if price is None or size is None:
68
+ continue
69
+ entries.append(
70
+ {
71
+ "s": str(symbol),
72
+ "S": side,
73
+ "p": str(price),
74
+ "q": str(size),
75
+ }
76
+ )
77
+
78
+ if not entries:
79
+ return
80
+
81
+ self._find_and_delete({"s": str(symbol)})
82
+ self._insert(entries)
83
+
84
+ def sorted(
85
+ self, query: Item | None = None, limit: int | None = None
86
+ ) -> dict[str, list[Item]]:
87
+ return self._sorted(
88
+ item_key="S",
89
+ item_asc_key="a",
90
+ item_desc_key="b",
91
+ sort_key="p",
92
+ query=query,
93
+ limit=limit,
94
+ )
95
+
96
+
97
+ class Detail(DataStore):
98
+ """CoinW 合约信息数据存储。
99
+
100
+ 文档: https://www.coinw.com/api-doc/futures-trading/market/get-instrument-information
101
+ """
102
+
103
+ _KEYS = ["name"]
104
+
105
+ @staticmethod
106
+ def _transform(entry: dict[str, Any]) -> dict[str, Any] | None:
107
+ if not entry:
108
+ return None
109
+ transformed = dict(entry)
110
+ base = entry.get("name") or entry.get("base")
111
+ quote = entry.get("quote")
112
+ pricePrecision = entry.get("pricePrecision")
113
+ transformed['tick_size'] = 10 ** (-int(pricePrecision))
114
+ transformed['step_size'] = entry.get("oneLotSize")
115
+ if base and quote:
116
+ transformed.setdefault(
117
+ "symbol", f"{str(base).upper()}_{str(quote).upper()}"
118
+ )
119
+ return transformed
120
+
121
+ def _onresponse(self, data: Any) -> None:
122
+ if data is None:
123
+ self._clear()
124
+ return
125
+
126
+ entries: list[dict[str, Any]]
127
+ if isinstance(data, dict):
128
+ entries = data.get("data") or []
129
+ else:
130
+ entries = data
131
+
132
+ items: list[dict[str, Any]] = []
133
+ for entry in entries:
134
+ if not isinstance(entry, dict):
135
+ continue
136
+ transformed = self._transform(entry)
137
+ if transformed:
138
+ items.append(transformed)
139
+
140
+ self._clear()
141
+ if items:
142
+ self._insert(items)
143
+
144
+
145
+ class Orders(DataStore):
146
+ """CoinW 当前订单数据存储。"""
147
+
148
+ _KEYS = ["id"]
149
+
150
+ @staticmethod
151
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
152
+ order_id = entry.get("id")
153
+ if order_id is None:
154
+ return None
155
+ normalized = dict(entry)
156
+ normalized["id"] = str(order_id)
157
+ return normalized
158
+
159
+ def _onresponse(self, data: Any) -> None:
160
+ payload = []
161
+ if isinstance(data, dict):
162
+ inner = data.get("data")
163
+ if isinstance(inner, dict):
164
+ payload = inner.get("rows") or []
165
+ elif isinstance(inner, list):
166
+ payload = inner
167
+ elif isinstance(data, list):
168
+ payload = data
169
+
170
+ items: list[dict[str, Any]] = []
171
+ for entry in payload or []:
172
+ if not isinstance(entry, dict):
173
+ continue
174
+ normalized = self._normalize(entry)
175
+ if normalized:
176
+ items.append(normalized)
177
+
178
+ self._clear()
179
+ if items:
180
+ self._insert(items)
181
+
182
+ def _on_message(self, msg: dict[str, Any]) -> None:
183
+ data = msg.get("data")
184
+ if isinstance(data, dict) and data.get("result") is not None:
185
+ return
186
+
187
+ entries: list[dict[str, Any]] = []
188
+ if isinstance(data, list):
189
+ entries = data
190
+ elif isinstance(data, dict):
191
+ entries = data.get("rows") or data.get("data") or []
192
+
193
+ if not entries:
194
+ return
195
+
196
+ to_insert: list[dict[str, Any]] = []
197
+ to_delete: list[dict[str, Any]] = []
198
+
199
+ for entry in entries:
200
+ if not isinstance(entry, dict):
201
+ continue
202
+ normalized = self._normalize(entry)
203
+ if not normalized:
204
+ continue
205
+
206
+ status = str(normalized.get("status") or "").lower()
207
+ order_status = str(normalized.get("orderStatus") or "").lower()
208
+ remove = status in {"close", "cancel", "canceled"} or order_status in {
209
+ "finish",
210
+ "cancel",
211
+ }
212
+
213
+ # query = {"id": normalized["id"]}
214
+ to_delete.append(normalized)
215
+ if not remove:
216
+ to_insert.append(normalized)
217
+
218
+ if to_delete:
219
+ self._delete(to_delete)
220
+ if to_insert:
221
+ self._insert(to_insert)
222
+
223
+
224
+ class Position(DataStore):
225
+ """CoinW 当前持仓数据存储。"""
226
+
227
+ _KEYS = ["id"]
228
+
229
+ @staticmethod
230
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
231
+ position_id = entry.get("id")
232
+ if position_id is None:
233
+ return None
234
+ normalized = dict(entry)
235
+ normalized["id"] = str(position_id)
236
+ return normalized
237
+
238
+ def _onresponse(self, data: Any) -> None:
239
+ payload = []
240
+ if isinstance(data, dict):
241
+ payload = data.get("data") or []
242
+ elif isinstance(data, list):
243
+ payload = data
244
+
245
+ items: list[dict[str, Any]] = []
246
+ for entry in payload or []:
247
+ if not isinstance(entry, dict):
248
+ continue
249
+ normalized = self._normalize(entry)
250
+ if normalized:
251
+ items.append(normalized)
252
+
253
+ self._clear()
254
+ if items:
255
+ self._insert(items)
256
+
257
+ def _on_message(self, msg: dict[str, Any]) -> None:
258
+ data = msg.get("data")
259
+ if isinstance(data, dict) and data.get("result") is not None:
260
+ return
261
+
262
+ entries: list[dict[str, Any]] = []
263
+ if isinstance(data, list):
264
+ entries = data
265
+ elif isinstance(data, dict):
266
+ entries = data.get("rows") or data.get("data") or []
267
+
268
+ if not entries:
269
+ return
270
+
271
+ to_insert: list[dict[str, Any]] = []
272
+ to_delete: list[dict[str, Any]] = []
273
+
274
+ for entry in entries:
275
+ if not isinstance(entry, dict):
276
+ continue
277
+ normalized = self._normalize(entry)
278
+ if not normalized:
279
+ continue
280
+
281
+ status = normalized.get("status")
282
+ normalized_status = str(status).lower() if status is not None else ""
283
+ remove = normalized_status in {"close", "closed", "1", "2"}
284
+
285
+ query = {"id": normalized["id"]}
286
+ to_delete.append(query)
287
+ if not remove:
288
+ to_insert.append(normalized)
289
+
290
+ if to_delete:
291
+ self._delete(to_delete)
292
+ if to_insert:
293
+ self._insert(to_insert)
294
+
295
+
296
+ class Balance(DataStore):
297
+ """CoinW 合约账户资产数据存储。"""
298
+
299
+ _KEYS = ["currency"]
300
+
301
+ @staticmethod
302
+ def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any]:
303
+ currency = "USDT"
304
+ normalized = {
305
+ "currency": currency,
306
+ "availableMargin": entry.get("availableMargin"),
307
+ "availableUsdt": entry.get("availableUsdt"),
308
+ "almightyGold": entry.get("almightyGold"),
309
+ "alMargin": entry.get("alMargin"),
310
+ "alFreeze": entry.get("alFreeze"),
311
+ "time": entry.get("time"),
312
+ "userId": entry.get("userId"),
313
+ }
314
+ if "available" not in normalized:
315
+ normalized["available"] = entry.get("availableUsdt")
316
+ normalized["availableMargin"] = entry.get("availableMargin")
317
+ normalized["margin"] = entry.get("alMargin")
318
+ normalized["freeze"] = entry.get("alFreeze")
319
+ return {k: v for k, v in normalized.items() if v is not None}
320
+
321
+ @staticmethod
322
+ def _normalize_ws(entry: dict[str, Any]) -> dict[str, Any] | None:
323
+ currency = entry.get("currency")
324
+ if not currency:
325
+ return None
326
+ currency_str = str(currency).upper()
327
+ normalized = dict(entry)
328
+ normalized["currency"] = currency_str
329
+ # 对齐 REST 字段
330
+ if "availableUsdt" not in normalized and "available" in normalized:
331
+ normalized["availableUsdt"] = normalized["available"]
332
+ if "alMargin" not in normalized and "margin" in normalized:
333
+ normalized["alMargin"] = normalized["margin"]
334
+ if "alFreeze" not in normalized and "freeze" in normalized:
335
+ normalized["alFreeze"] = normalized["freeze"]
336
+ return normalized
337
+
338
+ def _onresponse(self, data: Any) -> None:
339
+ entry = None
340
+ if isinstance(data, dict):
341
+ entry = data.get("data")
342
+ if not isinstance(entry, dict):
343
+ entry = {}
344
+
345
+ self._clear()
346
+ normalized = self._normalize_rest(entry)
347
+ self._insert([normalized])
348
+
349
+ def _on_message(self, msg: dict[str, Any]) -> None:
350
+ data = msg.get("data")
351
+ if isinstance(data, dict) and data.get("result") is not None:
352
+ return
353
+
354
+ entries: list[dict[str, Any]] = []
355
+ if isinstance(data, list):
356
+ entries = data
357
+ elif isinstance(data, dict):
358
+ entries = data.get("rows") or data.get("data") or []
359
+
360
+ if not entries:
361
+ return
362
+
363
+ normalized_items: list[dict[str, Any]] = []
364
+ for entry in entries:
365
+ if not isinstance(entry, dict):
366
+ continue
367
+ normalized = self._normalize_ws(entry)
368
+ if normalized:
369
+ normalized_items.append(normalized)
370
+
371
+ if not normalized_items:
372
+ return
373
+
374
+ currencies = [{"currency": item["currency"]} for item in normalized_items]
375
+ self._delete(currencies)
376
+ self._insert(normalized_items)
377
+
378
+
379
+ class CoinwFuturesDataStore(DataStoreCollection):
380
+ """CoinW 合约交易 DataStoreCollection。
381
+
382
+ - REST: https://api.coinw.com/v1/perpum/instruments
383
+ - WebSocket: wss://ws.futurescw.com/perpum (depth)
384
+ """
385
+
386
+ def _init(self) -> None:
387
+ self._create("book", datastore_class=Book)
388
+ self._create("detail", datastore_class=Detail)
389
+ self._create("orders", datastore_class=Orders)
390
+ self._create("position", datastore_class=Position)
391
+ self._create("balance", datastore_class=Balance)
392
+
393
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
394
+ msg_type = msg.get("type")
395
+ if msg_type == "depth":
396
+ self.book._on_message(msg)
397
+ elif msg_type == "order":
398
+ self.orders._on_message(msg)
399
+ elif msg_type == "position":
400
+ self.position._on_message(msg)
401
+ elif msg_type == "assets":
402
+ self.balance._on_message(msg)
403
+
404
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
405
+ for fut in asyncio.as_completed(aws):
406
+ res = await fut
407
+ data = await res.json()
408
+ if res.url.path == "/v1/perpum/instruments":
409
+ self.detail._onresponse(data)
410
+ elif res.url.path == "/v1/perpum/orders/open":
411
+ self.orders._onresponse(data)
412
+ elif res.url.path == "/v1/perpum/positions":
413
+ self.position._onresponse(data)
414
+ elif res.url.path == "/v1/perpum/account/getUserAssets":
415
+ self.balance._onresponse(data)
416
+
417
+ @property
418
+ def book(self) -> Book:
419
+ """订单簿深度数据流。
420
+
421
+ 数据来源:
422
+ - WebSocket: ``type == "depth"`` (参考 https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
423
+
424
+ 数据结构(节选)::
425
+
426
+ {
427
+ "s": "BTC",
428
+ "S": "a", # 卖单
429
+ "p": "95640.3",
430
+ "q": "0.807"
431
+ }
432
+ """
433
+
434
+ return self._get("book", Book)
435
+
436
+ @property
437
+ def detail(self) -> Detail:
438
+ """合约基础信息数据流。
439
+
440
+ 响应示例(节选):
441
+
442
+ .. code:: json
443
+
444
+ {
445
+ "base": "btc",
446
+ "closeSpread": 0.0002,
447
+ "commissionRate": 0.0006,
448
+ "configBo": {
449
+ "margins": {
450
+ "100": 0.075,
451
+ "5": 0.00375,
452
+ "50": 0.0375,
453
+ "20": 0.015,
454
+ "10": 0.0075
455
+ },
456
+ "simulatedMargins": {
457
+ "5": 0.00375,
458
+ "20": 0.015,
459
+ "10": 0.0075
460
+ }
461
+ },
462
+ "createdDate": 1548950400000,
463
+ "defaultLeverage": 20,
464
+ "defaultStopLossRate": 0.99,
465
+ "defaultStopProfitRate": 100,
466
+ "depthPrecision": "0.1,1,10,50,100",
467
+ "iconUrl": "https://hkto-prod.oss-accelerate.aliyuncs.com/4dfca512e957e14f05da07751a96061cf4bfd5df438504f65287fa0a8c3cadb6.svg",
468
+ "id": 1,
469
+ "indexId": 1,
470
+ "leverage": [
471
+ 5,
472
+ 10,
473
+ 20,
474
+ 50,
475
+ 100,
476
+ 125,
477
+ 200
478
+ ],
479
+ "makerFee": "0.0001",
480
+ "maxLeverage": 200,
481
+ "maxPosition": 20000,
482
+ "minLeverage": 1,
483
+ "minSize": 1,
484
+ "name": "BTC",
485
+ "oneLotMargin": 1,
486
+ "oneLotSize": 0.001,
487
+ "oneMaxPosition": 15000,
488
+ "openSpread": 0.0003,
489
+ "orderLimitMaxRate": 0.05,
490
+ "orderLimitMinRate": 0.05,
491
+ "orderMarketLimitAmount": 10,
492
+ "orderPlanLimitAmount": 30,
493
+ "partitionIds": "2013,2011",
494
+ "platform": 0,
495
+ "pricePrecision": 1,
496
+ "quote": "usdt",
497
+ "selected": 0,
498
+ "settledAt": 1761062400000,
499
+ "settledPeriod": 8,
500
+ "settlementRate": 0.0004,
501
+ "sort": 1,
502
+ "status": "online",
503
+ "stopCrossPositionRate": 0.1,
504
+ "stopSurplusRate": 0.01,
505
+ "takerFee": "0.0006",
506
+ "updatedDate": 1752040118000,
507
+ "symbol": "BTC_USDT",
508
+ "tick_size": 1.0,
509
+ "step_size": 0.001
510
+ }
511
+ """
512
+
513
+ return self._get("detail", Detail)
514
+
515
+ @property
516
+ def orders(self) -> Orders:
517
+ """当前订单数据流。
518
+
519
+ 数据来源:
520
+ - REST: ``GET /v1/perpum/orders/open``
521
+ - WebSocket: ``type == "order"``
522
+
523
+ 数据结构(节选)::
524
+
525
+ {
526
+ "currentPiece": "1",
527
+ "leverage": "50",
528
+ "originalType": "plan",
529
+ "processStatus": 0,
530
+ "contractType": 1,
531
+ "frozenFee": "0",
532
+ "openPrice": "175",
533
+ "orderStatus": "unFinish",
534
+ "instrument": "SOL",
535
+ "quantityUnit": 1,
536
+ "source": "web",
537
+ "updatedDate": 1761109078404,
538
+ "positionModel": 1,
539
+ "posType": "plan",
540
+ "baseSize": "0.1",
541
+ "quote": "usdt",
542
+ "liquidateBy": "manual",
543
+ "makerFee": "0.0001",
544
+ "totalPiece": "1",
545
+ "tradePiece": "0",
546
+ "orderPrice": "175",
547
+ "id": "33309055657317395",
548
+ "direction": "long",
549
+ "margin": "0.35",
550
+ "indexPrice": "185.68",
551
+ "quantity": "1",
552
+ "takerFee": "0.0006",
553
+ "userId": "1757458",
554
+ "cancelPiece": "0",
555
+ "createdDate": 1761109078404,
556
+ "positionMargin": "0.35",
557
+ "base": "sol",
558
+ "status": "open"
559
+ }
560
+ """
561
+
562
+ return self._get("orders", Orders)
563
+
564
+ @property
565
+ def position(self) -> Position:
566
+ """当前持仓数据流。
567
+
568
+ 数据来源:
569
+ - REST: ``GET /v1/perpum/positions``
570
+ - WebSocket: ``type == "position"``
571
+
572
+ 数据结构(节选)::
573
+
574
+ {
575
+ "id": 2435521222631982507,
576
+ "instrument": "BTC",
577
+ "direction": "short",
578
+ "openPrice": 88230.5,
579
+ "currentPiece": 1,
580
+ "profitUnreal": 0.0086,
581
+ "status": "open"
582
+ }
583
+ """
584
+
585
+ return self._get("position", Position)
586
+
587
+ @property
588
+ def balance(self) -> Balance:
589
+ """合约账户资产数据流。
590
+
591
+ 数据来源:
592
+ - REST: ``GET /v1/perpum/account/getUserAssets``
593
+ - WebSocket: ``type == "assets"``
594
+
595
+ .. code:: json
596
+
597
+ {
598
+ "currency": "USDT",
599
+ "availableMargin": 0.0,
600
+ "availableUsdt": 0,
601
+ "almightyGold": 0.0,
602
+ "alMargin": 0,
603
+ "alFreeze": 0,
604
+ "time": 1761055905797,
605
+ "userId": 1757458,
606
+ "available": 0,
607
+ "margin": 0,
608
+ "freeze": 0
609
+ }
610
+ """
611
+
612
+ return self._get("balance", Balance)
hyperquant/broker/ws.py CHANGED
@@ -1,13 +1,10 @@
1
1
  import asyncio
2
- import base64
3
2
  import time
4
3
  from typing import Any
5
4
 
6
5
  import pybotters
6
+ from aiohttp import WSMsgType
7
7
  from pybotters.ws import ClientWebSocketResponse, logger
8
- from pybotters.auth import Hosts
9
- import urllib
10
- import yarl
11
8
 
12
9
 
13
10
  class Heartbeat:
@@ -34,11 +31,18 @@ class Heartbeat:
34
31
  while not ws.closed:
35
32
  await ws.send_str('ping')
36
33
  await asyncio.sleep(6)
34
+
35
+ @staticmethod
36
+ async def coinw(ws: ClientWebSocketResponse):
37
+ while not ws.closed:
38
+ await ws.send_json({"event": "ping"})
39
+ await asyncio.sleep(10.0)
37
40
 
38
41
  pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
39
42
  pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
40
43
  pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
41
44
  pybotters.ws.HeartbeatHosts.items['uuws.rerrkvifj.com'] = Heartbeat.lbank
45
+ pybotters.ws.HeartbeatHosts.items['ws.futurescw.com'] = Heartbeat.coinw
42
46
 
43
47
  class WssAuth:
44
48
  @staticmethod
@@ -63,4 +67,49 @@ class WssAuth:
63
67
  else:
64
68
  logger.warning(f"WebSocket login failed: {data}")
65
69
 
70
+ @staticmethod
71
+ async def coinw(ws: ClientWebSocketResponse):
72
+ creds = ws._response._session.__dict__["_apis"].get(
73
+ pybotters.ws.AuthHosts.items[ws._response.url.host].name
74
+ )
75
+ if not creds:
76
+ raise RuntimeError("CoinW credentials are required for websocket login.")
77
+ if isinstance(creds, dict):
78
+ raise RuntimeError("CoinW credentials must be a sequence, not a dict.")
79
+ if len(creds) < 1:
80
+ raise RuntimeError("CoinW credentials are incomplete.")
81
+
82
+ api_key = creds[0]
83
+ secret = creds[1] if len(creds) > 1 else ""
84
+
85
+ await ws.send_json(
86
+ {
87
+ "event": "login",
88
+ "params": {
89
+ "api_key": api_key,
90
+ "passphrase": secret.decode(),
91
+ },
92
+ }
93
+ )
94
+
95
+ async for msg in ws:
96
+ if msg.type != WSMsgType.TEXT:
97
+ continue
98
+ try:
99
+ data:dict = msg.json()
100
+ except Exception: # pragma: no cover - defensive
101
+ continue
102
+
103
+ channel = data.get("channel")
104
+ event_type = data.get("type")
105
+ if channel == "login" or event_type == "login":
106
+ result = data.get("data", {}).get("result")
107
+ if result is not True:
108
+ raise RuntimeError(f"CoinW WebSocket login failed: {data}")
109
+ break
110
+ if data.get("event") == "pong":
111
+ # ignore heartbeat responses while waiting
112
+ continue
113
+
66
114
  pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
115
+ pybotters.ws.AuthHosts.items['ws.futurescw.com'] = pybotters.auth.Item("coinw", WssAuth.coinw)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.81
3
+ Version: 0.83
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
@@ -4,18 +4,20 @@ 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=IIWbqgflJzCcd1Se5W6vT6jjPiiLmjYmWGdiNb6JXbM,7696
7
+ hyperquant/broker/auth.py,sha256=xNZEQP0LRRV9BkT2uXBJ-vFfeahUnRVq1bjIT6YbQu8,10089
8
8
  hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
9
+ hyperquant/broker/coinw.py,sha256=iNAQQ562KeIBbADgyAarTMDwCXdA9qO3-LwFBPvxH-U,14023
9
10
  hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
10
11
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
11
- hyperquant/broker/lbank.py,sha256=npacHfV0VYJCBg8BX2lGa-ngMxY-lQE6cB_a0WL2iwA,22201
12
+ hyperquant/broker/lbank.py,sha256=98M5wmSoeHwbBYMA3rh25zqLb6fQKVaEmwqALF5nOvY,22181
12
13
  hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
13
- hyperquant/broker/ws.py,sha256=9Zu5JSLj-ylYEVmFmRwvZDDnVYKwb37cLHfZzA0AZGc,2200
14
+ hyperquant/broker/ws.py,sha256=Ozr5gBzVQ7cnxajT0MdpyKNyVgoCUu6LLjErU9fZxgc,4080
14
15
  hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
15
16
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
16
17
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
17
18
  hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
18
19
  hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
20
+ hyperquant/broker/models/coinw.py,sha256=iEBMTiZ_UjjlwhUGA74e6vvR2pXCl1ZoGKSyBPgqTsw,18658
19
21
  hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
20
22
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
21
23
  hyperquant/broker/models/lbank.py,sha256=vHkNKxIMzpoC_EwcZnEOPOupizF92yGWi9GKxvYYFUQ,19181
@@ -24,6 +26,6 @@ hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw
24
26
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
25
27
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
26
28
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
27
- hyperquant-0.81.dist-info/METADATA,sha256=JAd-E7Z2dt0T4CciFdfHuERvoFXMfSWDbIi_9SOlhAk,4317
28
- hyperquant-0.81.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- hyperquant-0.81.dist-info/RECORD,,
29
+ hyperquant-0.83.dist-info/METADATA,sha256=SlHTbSAsclL15RMQjveqABInk7gil66sZljrdp0IUlk,4317
30
+ hyperquant-0.83.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ hyperquant-0.83.dist-info/RECORD,,