hyperquant 0.82__py3-none-any.whl → 0.84__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
@@ -198,6 +198,75 @@ class Auth:
198
198
 
199
199
  return args
200
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
+
201
270
  pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
202
271
  "ourbit", Auth.ourbit
203
272
  )
@@ -220,4 +289,8 @@ pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
220
289
 
221
290
  pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
222
291
  "lbank", Auth.lbank
223
- )
292
+ )
293
+
294
+ pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
295
+ "coinw", Auth.coinw
296
+ )
@@ -0,0 +1,411 @@
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
+ "ticker",
56
+ "orders",
57
+ "position",
58
+ "balance",
59
+ "all",
60
+ ] = "all",
61
+ *,
62
+ instrument: str | None = None,
63
+ position_type: Literal["execute", "plan", "planTrigger"] = "execute",
64
+ page: int | None = None,
65
+ page_size: int | None = None,
66
+ open_ids: str | None = None,
67
+ ) -> None:
68
+ """刷新本地缓存,使用 CoinW REST API。
69
+
70
+ - detail: ``GET /v1/perpum/instruments`` (公共)
71
+ - ticker: ``GET /v1/perpumPublic/tickers`` (公共)
72
+ - orders: ``GET /v1/perpum/orders/open`` (私有,需要 ``instrument``)
73
+ - position: ``GET /v1/perpum/positions`` (私有,需要 ``instrument``)
74
+ - balance: ``GET /v1/perpum/account/getUserAssets`` (私有)
75
+ """
76
+
77
+ requests: list[Any] = []
78
+
79
+ include_detail = update_type in {"detail", "all"}
80
+ include_ticker = update_type in {"ticker", "all"}
81
+ include_orders = update_type in {"orders", "all"}
82
+ include_position = update_type in {"position", "all"}
83
+ include_balance = update_type in {"balance", "all"}
84
+
85
+ if include_detail:
86
+ requests.append(self.client.get(f"{self.rest_api}/v1/perpum/instruments"))
87
+
88
+ if include_ticker:
89
+ requests.append(self.client.get(f"{self.rest_api}/v1/perpumPublic/tickers"))
90
+
91
+ if include_orders:
92
+ if not instrument:
93
+ raise ValueError("instrument is required when updating orders")
94
+ params: dict[str, Any] = {
95
+ "instrument": instrument,
96
+ "positionType": position_type,
97
+ }
98
+ if page is not None:
99
+ params["page"] = page
100
+ if page_size is not None:
101
+ params["pageSize"] = page_size
102
+ requests.append(
103
+ self.client.get(
104
+ f"{self.rest_api}/v1/perpum/orders/open",
105
+ params=params,
106
+ )
107
+ )
108
+
109
+ if include_position:
110
+ if not instrument:
111
+ raise ValueError("instrument is required when updating positions")
112
+ params = {"instrument": instrument}
113
+ if open_ids:
114
+ params["openIds"] = open_ids
115
+ requests.append(
116
+ self.client.get(
117
+ f"{self.rest_api}/v1/perpum/positions",
118
+ params=params,
119
+ )
120
+ )
121
+
122
+ if include_balance:
123
+ requests.append(
124
+ self.client.get(f"{self.rest_api}/v1/perpum/account/getUserAssets")
125
+ )
126
+
127
+ if not requests:
128
+ raise ValueError(f"update_type err: {update_type}")
129
+
130
+ await self.store.initialize(*requests)
131
+
132
+ async def place_order(
133
+ self,
134
+ instrument: str,
135
+ *,
136
+ direction: Literal["long", "short"],
137
+ leverage: int,
138
+ quantity: float | str,
139
+ quantity_unit: Literal[0, 1, 2, "quote", "contract", "base"] = 0,
140
+ position_model: Literal[0, 1, "isolated", "cross"] = 0,
141
+ position_type: Literal["execute", "plan", "planTrigger"] = "execute",
142
+ price: float | None = None,
143
+ trigger_price: float | None = None,
144
+ trigger_type: Literal[0, 1] | None = None,
145
+ stop_loss_price: float | None = None,
146
+ stop_profit_price: float | None = None,
147
+ third_order_id: str | None = None,
148
+ use_almighty_gold: bool | None = None,
149
+ gold_id: int | None = None,
150
+ ) -> dict[str, Any]:
151
+ """``POST /v1/perpum/order`` 下单。"""
152
+
153
+ payload: dict[str, Any] = {
154
+ "instrument": instrument,
155
+ "direction": self._normalize_direction(direction),
156
+ "leverage": int(leverage),
157
+ "quantityUnit": self._normalize_quantity_unit(quantity_unit),
158
+ "quantity": self._format_quantity(quantity),
159
+ "positionModel": self._normalize_position_model(position_model),
160
+ "positionType": position_type,
161
+ }
162
+
163
+ if price is not None:
164
+ payload["openPrice"] = price
165
+ if trigger_price is not None:
166
+ payload["triggerPrice"] = trigger_price
167
+ if trigger_type is not None:
168
+ payload["triggerType"] = int(trigger_type)
169
+ if stop_loss_price is not None:
170
+ payload["stopLossPrice"] = stop_loss_price
171
+ if stop_profit_price is not None:
172
+ payload["stopProfitPrice"] = stop_profit_price
173
+ if third_order_id:
174
+ payload["thirdOrderId"] = third_order_id
175
+ if use_almighty_gold is not None:
176
+ payload["useAlmightyGold"] = int(bool(use_almighty_gold))
177
+ if gold_id is not None:
178
+ payload["goldId"] = int(gold_id)
179
+
180
+ res = await self.client.post(
181
+ f"{self.rest_api}/v1/perpum/order",
182
+ data=payload,
183
+ )
184
+
185
+ data = await res.json()
186
+ return self._ensure_ok("place_order", data)
187
+
188
+ async def place_order_web(
189
+ self,
190
+ instrument: str,
191
+ *,
192
+ direction: Literal["long", "short"],
193
+ leverage: int | str,
194
+ quantity_unit: Literal[0, 1, 2],
195
+ quantity: str | float | int,
196
+ position_model: Literal[0, 1] = 1,
197
+ position_type: Literal["plan", "planTrigger", "execute"] = 'plan',
198
+ open_price: str | float | None = None,
199
+ contract_type: int = 1,
200
+ data_type: str = "trade_take",
201
+ device_id: str,
202
+ token: str,
203
+ headers: dict[str, str] | None = None,
204
+ ) -> dict[str, Any]:
205
+ """使用 Web 前端接口下单,绕过部分 API 频控策略。
206
+
207
+ 注意此接口需要传入真实浏览器参数,如 ``device_id`` 与 ``token``。
208
+ """
209
+
210
+ if not device_id or not token:
211
+ raise ValueError("device_id and token are required for place_order_web")
212
+
213
+ url = f"{self.web_api}/v1/futuresc/thirdClient/trade/{instrument}/open"
214
+
215
+ payload: dict[str, Any] = {
216
+ "instrument": instrument,
217
+ "direction": direction,
218
+ "leverage": str(leverage),
219
+ "quantityUnit": quantity_unit,
220
+ "quantity": str(quantity),
221
+ "positionModel": position_model,
222
+ "positionType": position_type,
223
+ "contractType": contract_type,
224
+ "dataType": data_type,
225
+ }
226
+ if open_price is not None:
227
+ payload["openPrice"] = str(open_price)
228
+
229
+ base_headers = {
230
+ "accept": "application/json, text/plain, */*",
231
+ "accept-language": "zh_CN",
232
+ "appversion": "100.100.100",
233
+ "cache-control": "no-cache",
234
+ "clienttag": "web",
235
+ "content-type": "application/json",
236
+ "cwdeviceid": device_id,
237
+ "deviceid": device_id,
238
+ "devicename": "Chrome V141.0.0.0 (macOS)",
239
+ "language": "zh_CN",
240
+ "logintoken": token,
241
+ "origin": "https://www.coinw.com",
242
+ "pragma": "no-cache",
243
+ "priority": "u=1, i",
244
+ "referer": "https://www.coinw.com/",
245
+ "sec-ch-ua": '"Microsoft Edge";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
246
+ "sec-ch-ua-mobile": "?0",
247
+ "sec-ch-ua-platform": '"macOS"',
248
+ "sec-fetch-dest": "empty",
249
+ "sec-fetch-mode": "cors",
250
+ "sec-fetch-site": "same-site",
251
+ "selecttype": "USD",
252
+ "systemversion": "macOS 10.15.7",
253
+ "thirdappid": "coinw",
254
+ "thirdapptoken": token,
255
+ "token": token,
256
+ "user-agent": (
257
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
258
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 "
259
+ "Safari/537.36 Edg/141.0.0.0"
260
+ ),
261
+ "withcredentials": "true",
262
+ "x-authorization": token,
263
+ "x-language": "zh_CN",
264
+ "x-locale": "zh_CN",
265
+ "x-requested-with": "XMLHttpRequest",
266
+ }
267
+ if headers:
268
+ base_headers.update(headers)
269
+
270
+ res = await self.client.post(
271
+ url,
272
+ json=payload,
273
+ headers=base_headers,
274
+ auth=None,
275
+ )
276
+ return await res.json()
277
+
278
+ async def cancel_order(self, order_id: str | int) -> dict[str, Any]:
279
+ """``DELETE /v1/perpum/order`` 取消单个订单。"""
280
+
281
+ res = await self.client.delete(
282
+ f"{self.rest_api}/v1/perpum/order",
283
+ data={"id": str(order_id)},
284
+ )
285
+ data = await res.json()
286
+ return self._ensure_ok("cancel_order", data)
287
+
288
+ async def sub_personal(self) -> None:
289
+ """订阅订单、持仓、资产私有频道。"""
290
+
291
+ ws_app = await self._ensure_private_ws()
292
+ payloads = [
293
+ {"event": "sub", "params": {"biz": "futures", "type": "order"}},
294
+ {"event": "sub", "params": {"biz": "futures", "type": "position"}},
295
+ {"event": "sub", "params": {"biz": "futures", "type": "position_change"}},
296
+ {"event": "sub", "params": {"biz": "futures", "type": "assets"}},
297
+ ]
298
+ for payload in payloads:
299
+ if ws_app.current_ws.closed:
300
+ raise ConnectionError("CoinW private websocket closed before subscription.")
301
+ await ws_app.current_ws.send_json(payload)
302
+ await asyncio.sleep(0.05)
303
+
304
+ async def sub_orderbook(
305
+ self,
306
+ pair_codes: Sequence[str] | str,
307
+ *,
308
+ depth_limit: int | None = None,
309
+ biz: str = "futures",
310
+ ) -> pybotters.ws.WebSocketApp:
311
+ """订阅 ``type=depth`` 订单簿数据,批量控制发送频率。"""
312
+
313
+ if isinstance(pair_codes, str):
314
+ pair_codes = [pair_codes]
315
+
316
+ pair_list = [code for code in pair_codes if code]
317
+ if not pair_list:
318
+ raise ValueError("pair_codes must not be empty")
319
+
320
+ self.store.book.limit = depth_limit
321
+
322
+ subscriptions = [
323
+ {"event": "sub", "params": {"biz": biz, "type": "depth", "pairCode": code}}
324
+ for code in pair_list
325
+ ]
326
+
327
+ ws_app = self.client.ws_connect(
328
+ self.ws_url_public,
329
+ hdlr_json=self.store.onmessage,
330
+ headers=self._ws_headers,
331
+ )
332
+ await ws_app._event.wait()
333
+
334
+ chunk_size = 10
335
+ for idx in range(0, len(subscriptions), chunk_size):
336
+ batch = subscriptions[idx : idx + chunk_size]
337
+ for msg in batch:
338
+ await ws_app.current_ws.send_json(msg)
339
+ if idx + chunk_size < len(subscriptions):
340
+ await asyncio.sleep(2.05)
341
+
342
+ return ws_app
343
+
344
+ async def _ensure_private_ws(self) -> pybotters.ws.WebSocketApp:
345
+
346
+ ws_app = self.client.ws_connect(
347
+ self.ws_url_private,
348
+ hdlr_json=self.store.onmessage,
349
+ headers=self._ws_headers,
350
+ )
351
+ await ws_app._event.wait()
352
+ await ws_app.current_ws._wait_authtask()
353
+
354
+ return ws_app
355
+
356
+ @staticmethod
357
+ def _normalize_direction(direction: str) -> str:
358
+ allowed = {"long", "short"}
359
+ value = str(direction).lower()
360
+ if value not in allowed:
361
+ raise ValueError(f"Unsupported direction: {direction}")
362
+ return value
363
+
364
+ @staticmethod
365
+ def _normalize_quantity_unit(
366
+ unit: Literal[0, 1, 2, "quote", "contract", "base"],
367
+ ) -> int:
368
+ mapping = {
369
+ 0: 0,
370
+ 1: 1,
371
+ 2: 2,
372
+ "quote": 0,
373
+ "contract": 1,
374
+ "base": 2,
375
+ }
376
+ try:
377
+ return mapping[unit] # type: ignore[index]
378
+ except KeyError as exc: # pragma: no cover - guard
379
+ raise ValueError(f"Unsupported quantity_unit: {unit}") from exc
380
+
381
+ @staticmethod
382
+ def _normalize_position_model(
383
+ model: Literal[0, 1, "isolated", "cross"],
384
+ ) -> int:
385
+ mapping = {
386
+ 0: 0,
387
+ 1: 1,
388
+ "isolated": 0,
389
+ "cross": 1,
390
+ }
391
+ try:
392
+ return mapping[model] # type: ignore[index]
393
+ except KeyError as exc: # pragma: no cover - guard
394
+ raise ValueError(f"Unsupported position_model: {model}") from exc
395
+
396
+ @staticmethod
397
+ def _format_quantity(quantity: float | str) -> str:
398
+ if isinstance(quantity, str):
399
+ return quantity
400
+ return str(quantity)
401
+
402
+ @staticmethod
403
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
404
+ """CoinW REST 成功时返回 ``{'code': 0, ...}``。"""
405
+
406
+ if not isinstance(data, dict) or data.get("code") != 0:
407
+ raise RuntimeError(f"{operation} failed: {data}")
408
+ payload = data.get("data")
409
+ if isinstance(payload, dict):
410
+ return payload
411
+ return {"data": payload}
@@ -0,0 +1,691 @@
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 Ticker(DataStore):
146
+ """CoinW 24h 交易摘要数据存储。
147
+
148
+ 文档: https://www.coinw.com/api-doc/futures-trading/market/get-last-trade-summary-of-all-instruments
149
+ """
150
+
151
+ _KEYS = ["name"]
152
+
153
+ @staticmethod
154
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
155
+ instrument = entry.get("instrument") or entry.get("symbol") or entry.get("pairCode")
156
+ if not instrument:
157
+ return None
158
+ normalized = dict(entry)
159
+ normalized["instrument"] = str(instrument).upper()
160
+ return normalized
161
+
162
+ def _onresponse(self, data: Any) -> None:
163
+ if isinstance(data, dict):
164
+ entries = data.get("data") or []
165
+ else:
166
+ entries = data
167
+
168
+ self._update(entries)
169
+
170
+ def _on_message(self, msg: dict[str, Any]) -> None:
171
+ data = msg.get("data")
172
+ entries: list[dict[str, Any]] = []
173
+ if isinstance(data, list):
174
+ entries = data
175
+ elif isinstance(data, dict):
176
+ entries = data.get("data") or data.get("tickers") or []
177
+
178
+ items: list[dict[str, Any]] = []
179
+ for entry in entries:
180
+ if not isinstance(entry, dict):
181
+ continue
182
+ normalized = self._normalize(entry)
183
+ if normalized:
184
+ items.append(normalized)
185
+
186
+ if not items:
187
+ return
188
+
189
+ instruments = [{"instrument": item["instrument"]} for item in items]
190
+ self._delete(instruments)
191
+ self._insert(items)
192
+
193
+
194
+ class Orders(DataStore):
195
+ """CoinW 当前订单数据存储。"""
196
+
197
+ _KEYS = ["id"]
198
+
199
+ @staticmethod
200
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
201
+ order_id = entry.get("id")
202
+ if order_id is None:
203
+ return None
204
+ normalized = dict(entry)
205
+ normalized["id"] = str(order_id)
206
+ return normalized
207
+
208
+ def _onresponse(self, data: Any) -> None:
209
+ payload = []
210
+ if isinstance(data, dict):
211
+ inner = data.get("data")
212
+ if isinstance(inner, dict):
213
+ payload = inner.get("rows") or []
214
+ elif isinstance(inner, list):
215
+ payload = inner
216
+ elif isinstance(data, list):
217
+ payload = data
218
+
219
+ items: list[dict[str, Any]] = []
220
+ for entry in payload or []:
221
+ if not isinstance(entry, dict):
222
+ continue
223
+ normalized = self._normalize(entry)
224
+ if normalized:
225
+ items.append(normalized)
226
+
227
+ self._clear()
228
+ if items:
229
+ self._insert(items)
230
+
231
+ def _on_message(self, msg: dict[str, Any]) -> None:
232
+ data = msg.get("data")
233
+ if isinstance(data, dict) and data.get("result") is not None:
234
+ return
235
+
236
+ entries: list[dict[str, Any]] = []
237
+ if isinstance(data, list):
238
+ entries = data
239
+ elif isinstance(data, dict):
240
+ entries = data.get("rows") or data.get("data") or []
241
+
242
+ if not entries:
243
+ return
244
+
245
+ to_insert: list[dict[str, Any]] = []
246
+ to_delete: list[dict[str, Any]] = []
247
+
248
+ for entry in entries:
249
+ if not isinstance(entry, dict):
250
+ continue
251
+ normalized = self._normalize(entry)
252
+ if not normalized:
253
+ continue
254
+
255
+ status = str(normalized.get("status") or "").lower()
256
+ order_status = str(normalized.get("orderStatus") or "").lower()
257
+ remove = status in {"close", "cancel", "canceled"} or order_status in {
258
+ "finish",
259
+ "cancel",
260
+ }
261
+
262
+ # query = {"id": normalized["id"]}
263
+ to_delete.append(normalized)
264
+ if not remove:
265
+ to_insert.append(normalized)
266
+
267
+ if to_delete:
268
+ self._delete(to_delete)
269
+ if to_insert:
270
+ self._insert(to_insert)
271
+
272
+
273
+ class Position(DataStore):
274
+ """CoinW 当前持仓数据存储。"""
275
+
276
+ _KEYS = ["id"]
277
+
278
+ @staticmethod
279
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
280
+ position_id = entry.get("id")
281
+ if position_id is None:
282
+ return None
283
+ normalized = dict(entry)
284
+ normalized["id"] = str(position_id)
285
+ return normalized
286
+
287
+ def _onresponse(self, data: Any) -> None:
288
+ payload = []
289
+ if isinstance(data, dict):
290
+ payload = data.get("data") or []
291
+ elif isinstance(data, list):
292
+ payload = data
293
+
294
+ items: list[dict[str, Any]] = []
295
+ for entry in payload or []:
296
+ if not isinstance(entry, dict):
297
+ continue
298
+ normalized = self._normalize(entry)
299
+ if normalized:
300
+ items.append(normalized)
301
+
302
+ self._clear()
303
+ if items:
304
+ self._insert(items)
305
+
306
+ def _on_message(self, msg: dict[str, Any]) -> None:
307
+ data = msg.get("data")
308
+ if isinstance(data, dict) and data.get("result") is not None:
309
+ return
310
+
311
+ entries: list[dict[str, Any]] = []
312
+ if isinstance(data, list):
313
+ entries = data
314
+ elif isinstance(data, dict):
315
+ entries = data.get("rows") or data.get("data") or []
316
+
317
+ if not entries:
318
+ return
319
+
320
+ to_insert: list[dict[str, Any]] = []
321
+ to_delete: list[dict[str, Any]] = []
322
+
323
+ for entry in entries:
324
+ if not isinstance(entry, dict):
325
+ continue
326
+ normalized = self._normalize(entry)
327
+ if not normalized:
328
+ continue
329
+
330
+ status = normalized.get("status")
331
+ normalized_status = str(status).lower() if status is not None else ""
332
+ remove = normalized_status in {"close", "closed", "1", "2"}
333
+
334
+ query = {"id": normalized["id"]}
335
+ to_delete.append(query)
336
+ if not remove:
337
+ to_insert.append(normalized)
338
+
339
+ if to_delete:
340
+ self._delete(to_delete)
341
+ if to_insert:
342
+ self._insert(to_insert)
343
+
344
+
345
+ class Balance(DataStore):
346
+ """CoinW 合约账户资产数据存储。"""
347
+
348
+ _KEYS = ["currency"]
349
+
350
+ @staticmethod
351
+ def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any]:
352
+ currency = "USDT"
353
+ normalized = {
354
+ "currency": currency,
355
+ "availableMargin": entry.get("availableMargin"),
356
+ "availableUsdt": entry.get("availableUsdt"),
357
+ "almightyGold": entry.get("almightyGold"),
358
+ "alMargin": entry.get("alMargin"),
359
+ "alFreeze": entry.get("alFreeze"),
360
+ "time": entry.get("time"),
361
+ "userId": entry.get("userId"),
362
+ }
363
+ if "available" not in normalized:
364
+ normalized["available"] = entry.get("availableUsdt")
365
+ normalized["availableMargin"] = entry.get("availableMargin")
366
+ normalized["margin"] = entry.get("alMargin")
367
+ normalized["freeze"] = entry.get("alFreeze")
368
+ return {k: v for k, v in normalized.items() if v is not None}
369
+
370
+ @staticmethod
371
+ def _normalize_ws(entry: dict[str, Any]) -> dict[str, Any] | None:
372
+ currency = entry.get("currency")
373
+ if not currency:
374
+ return None
375
+ currency_str = str(currency).upper()
376
+ normalized = dict(entry)
377
+ normalized["currency"] = currency_str
378
+ # 对齐 REST 字段
379
+ if "availableUsdt" not in normalized and "available" in normalized:
380
+ normalized["availableUsdt"] = normalized["available"]
381
+ if "alMargin" not in normalized and "margin" in normalized:
382
+ normalized["alMargin"] = normalized["margin"]
383
+ if "alFreeze" not in normalized and "freeze" in normalized:
384
+ normalized["alFreeze"] = normalized["freeze"]
385
+ return normalized
386
+
387
+ def _onresponse(self, data: Any) -> None:
388
+ entry = None
389
+ if isinstance(data, dict):
390
+ entry = data.get("data")
391
+ if not isinstance(entry, dict):
392
+ entry = {}
393
+
394
+ self._clear()
395
+ normalized = self._normalize_rest(entry)
396
+ self._insert([normalized])
397
+
398
+ def _on_message(self, msg: dict[str, Any]) -> None:
399
+ data = msg.get("data")
400
+ if isinstance(data, dict) and data.get("result") is not None:
401
+ return
402
+
403
+ entries: list[dict[str, Any]] = []
404
+ if isinstance(data, list):
405
+ entries = data
406
+ elif isinstance(data, dict):
407
+ entries = data.get("rows") or data.get("data") or []
408
+
409
+ if not entries:
410
+ return
411
+
412
+ normalized_items: list[dict[str, Any]] = []
413
+ for entry in entries:
414
+ if not isinstance(entry, dict):
415
+ continue
416
+ normalized = self._normalize_ws(entry)
417
+ if normalized:
418
+ normalized_items.append(normalized)
419
+
420
+ if not normalized_items:
421
+ return
422
+
423
+ currencies = [{"currency": item["currency"]} for item in normalized_items]
424
+ self._delete(currencies)
425
+ self._insert(normalized_items)
426
+
427
+
428
+ class CoinwFuturesDataStore(DataStoreCollection):
429
+ """CoinW 合约交易 DataStoreCollection。
430
+
431
+ - REST: https://api.coinw.com/v1/perpum/instruments
432
+ - WebSocket: wss://ws.futurescw.com/perpum (depth)
433
+ """
434
+
435
+ def _init(self) -> None:
436
+ self._create("book", datastore_class=Book)
437
+ self._create("detail", datastore_class=Detail)
438
+ self._create("ticker", datastore_class=Ticker)
439
+ self._create("orders", datastore_class=Orders)
440
+ self._create("position", datastore_class=Position)
441
+ self._create("balance", datastore_class=Balance)
442
+
443
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
444
+ msg_type = msg.get("type")
445
+ if msg_type == "depth":
446
+ self.book._on_message(msg)
447
+ elif msg_type == "order":
448
+ self.orders._on_message(msg)
449
+ elif msg_type == "position":
450
+ self.position._on_message(msg)
451
+ elif msg_type == "assets":
452
+ self.balance._on_message(msg)
453
+ elif msg_type == "ticker":
454
+ self.ticker._on_message(msg)
455
+
456
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
457
+ for fut in asyncio.as_completed(aws):
458
+ res = await fut
459
+ data = await res.json()
460
+ if res.url.path == "/v1/perpum/instruments":
461
+ self.detail._onresponse(data)
462
+ elif res.url.path == "/v1/perpumPublic/tickers":
463
+ self.ticker._onresponse(data)
464
+ elif res.url.path == "/v1/perpum/orders/open":
465
+ self.orders._onresponse(data)
466
+ elif res.url.path == "/v1/perpum/positions":
467
+ self.position._onresponse(data)
468
+ elif res.url.path == "/v1/perpum/account/getUserAssets":
469
+ self.balance._onresponse(data)
470
+
471
+ @property
472
+ def book(self) -> Book:
473
+ """订单簿深度数据流。
474
+
475
+ 数据来源:
476
+ - WebSocket: ``type == "depth"`` (参考 https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
477
+
478
+ 数据结构(节选)::
479
+
480
+ {
481
+ "s": "BTC",
482
+ "S": "a", # 卖单
483
+ "p": "95640.3",
484
+ "q": "0.807"
485
+ }
486
+ """
487
+
488
+ return self._get("book", Book)
489
+
490
+ @property
491
+ def detail(self) -> Detail:
492
+ """合约基础信息数据流。
493
+
494
+ 响应示例(节选):
495
+
496
+ .. code:: json
497
+
498
+ {
499
+ "base": "btc",
500
+ "closeSpread": 0.0002,
501
+ "commissionRate": 0.0006,
502
+ "configBo": {
503
+ "margins": {
504
+ "100": 0.075,
505
+ "5": 0.00375,
506
+ "50": 0.0375,
507
+ "20": 0.015,
508
+ "10": 0.0075
509
+ },
510
+ "simulatedMargins": {
511
+ "5": 0.00375,
512
+ "20": 0.015,
513
+ "10": 0.0075
514
+ }
515
+ },
516
+ "createdDate": 1548950400000,
517
+ "defaultLeverage": 20,
518
+ "defaultStopLossRate": 0.99,
519
+ "defaultStopProfitRate": 100,
520
+ "depthPrecision": "0.1,1,10,50,100",
521
+ "iconUrl": "https://hkto-prod.oss-accelerate.aliyuncs.com/4dfca512e957e14f05da07751a96061cf4bfd5df438504f65287fa0a8c3cadb6.svg",
522
+ "id": 1,
523
+ "indexId": 1,
524
+ "leverage": [
525
+ 5,
526
+ 10,
527
+ 20,
528
+ 50,
529
+ 100,
530
+ 125,
531
+ 200
532
+ ],
533
+ "makerFee": "0.0001",
534
+ "maxLeverage": 200,
535
+ "maxPosition": 20000,
536
+ "minLeverage": 1,
537
+ "minSize": 1,
538
+ "name": "BTC",
539
+ "oneLotMargin": 1,
540
+ "oneLotSize": 0.001,
541
+ "oneMaxPosition": 15000,
542
+ "openSpread": 0.0003,
543
+ "orderLimitMaxRate": 0.05,
544
+ "orderLimitMinRate": 0.05,
545
+ "orderMarketLimitAmount": 10,
546
+ "orderPlanLimitAmount": 30,
547
+ "partitionIds": "2013,2011",
548
+ "platform": 0,
549
+ "pricePrecision": 1,
550
+ "quote": "usdt",
551
+ "selected": 0,
552
+ "settledAt": 1761062400000,
553
+ "settledPeriod": 8,
554
+ "settlementRate": 0.0004,
555
+ "sort": 1,
556
+ "status": "online",
557
+ "stopCrossPositionRate": 0.1,
558
+ "stopSurplusRate": 0.01,
559
+ "takerFee": "0.0006",
560
+ "updatedDate": 1752040118000,
561
+ "symbol": "BTC_USDT",
562
+ "tick_size": 1.0,
563
+ "step_size": 0.001
564
+ }
565
+ """
566
+
567
+ return self._get("detail", Detail)
568
+
569
+ @property
570
+ def ticker(self) -> Ticker:
571
+ """24小时交易摘要数据流。
572
+
573
+ .. code:: json
574
+
575
+ {
576
+ 'fair_price': 97072.4,
577
+ 'max_leverage': 125,
578
+ 'total_volume': 0.003,
579
+ 'price_coin': 'btc',
580
+ 'contract_id': 1,
581
+ 'base_coin': 'btc',
582
+ 'high': 98001.5,
583
+ 'rise_fall_rate': 0.012275,
584
+ 'low': 95371.4,
585
+ 'name': 'BTCUSDT',
586
+ 'contract_size': 0.001,
587
+ 'quote_coin': 'usdt',
588
+ 'last_price': 97072.4
589
+ }
590
+
591
+ """
592
+ return self._get("ticker", Ticker)
593
+
594
+ @property
595
+ def orders(self) -> Orders:
596
+ """当前订单数据流。
597
+
598
+ 数据来源:
599
+ - REST: ``GET /v1/perpum/orders/open``
600
+ - WebSocket: ``type == "order"``
601
+
602
+ 数据结构(节选)::
603
+
604
+ {
605
+ "currentPiece": "1",
606
+ "leverage": "50",
607
+ "originalType": "plan",
608
+ "processStatus": 0,
609
+ "contractType": 1,
610
+ "frozenFee": "0",
611
+ "openPrice": "175",
612
+ "orderStatus": "unFinish",
613
+ "instrument": "SOL",
614
+ "quantityUnit": 1,
615
+ "source": "web",
616
+ "updatedDate": 1761109078404,
617
+ "positionModel": 1,
618
+ "posType": "plan",
619
+ "baseSize": "0.1",
620
+ "quote": "usdt",
621
+ "liquidateBy": "manual",
622
+ "makerFee": "0.0001",
623
+ "totalPiece": "1",
624
+ "tradePiece": "0",
625
+ "orderPrice": "175",
626
+ "id": "33309055657317395",
627
+ "direction": "long",
628
+ "margin": "0.35",
629
+ "indexPrice": "185.68",
630
+ "quantity": "1",
631
+ "takerFee": "0.0006",
632
+ "userId": "1757458",
633
+ "cancelPiece": "0",
634
+ "createdDate": 1761109078404,
635
+ "positionMargin": "0.35",
636
+ "base": "sol",
637
+ "status": "open"
638
+ }
639
+ """
640
+
641
+ return self._get("orders", Orders)
642
+
643
+ @property
644
+ def position(self) -> Position:
645
+ """当前持仓数据流。
646
+
647
+ 数据来源:
648
+ - REST: ``GET /v1/perpum/positions``
649
+ - WebSocket: ``type == "position"``
650
+
651
+ 数据结构(节选)::
652
+
653
+ {
654
+ "id": 2435521222631982507,
655
+ "instrument": "BTC",
656
+ "direction": "short",
657
+ "openPrice": 88230.5,
658
+ "currentPiece": 1,
659
+ "profitUnreal": 0.0086,
660
+ "status": "open"
661
+ }
662
+ """
663
+
664
+ return self._get("position", Position)
665
+
666
+ @property
667
+ def balance(self) -> Balance:
668
+ """合约账户资产数据流。
669
+
670
+ 数据来源:
671
+ - REST: ``GET /v1/perpum/account/getUserAssets``
672
+ - WebSocket: ``type == "assets"``
673
+
674
+ .. code:: json
675
+
676
+ {
677
+ "currency": "USDT",
678
+ "availableMargin": 0.0,
679
+ "availableUsdt": 0,
680
+ "almightyGold": 0.0,
681
+ "alMargin": 0,
682
+ "alFreeze": 0,
683
+ "time": 1761055905797,
684
+ "userId": 1757458,
685
+ "available": 0,
686
+ "margin": 0,
687
+ "freeze": 0
688
+ }
689
+ """
690
+
691
+ 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.82
3
+ Version: 0.84
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=Wst7mTBuUS2BQ5hZd0a8FNNs5Uc01ac9WzJpseTuyAY,7673
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=tdzOGm7vffVet5qOGiNwrAyOve_DNouz2vo9Mau5MAI,14284
9
10
  hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
10
11
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
11
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=r1iGdZ6K0CPY_y54cilFbj0lWzmPnb-V9aiD96zDWww,21089
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.82.dist-info/METADATA,sha256=sxi128truWA3CtkwESMjdobKtg1hEOL3M5NoDkUGtUs,4317
28
- hyperquant-0.82.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- hyperquant-0.82.dist-info/RECORD,,
29
+ hyperquant-0.84.dist-info/METADATA,sha256=5j7OFb68ZwN1qXESUuRQuJTTIgVJesPe3vfPvPR9oT8,4317
30
+ hyperquant-0.84.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
+ hyperquant-0.84.dist-info/RECORD,,