hyperquant 0.89__py3-none-any.whl → 0.92__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.
@@ -0,0 +1,591 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from datetime import datetime, timezone
6
+ import hashlib
7
+ import json
8
+ from typing import Any, Literal, Sequence
9
+
10
+ import pybotters
11
+ import rnet
12
+
13
+ from .models.coinup import CoinUpDataStore
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _DEFAULT_SECURITY_INFO = (
18
+ '{"log_BSDeviceFingerprint":"0","log_original":"0","log_CHFIT_DEVICEID":"0"}'
19
+ )
20
+ _SECRET_PREFIX = "HJ@%*AZ_J"
21
+ _DEFAULT_UA = (
22
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
23
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
24
+ "Chrome/141.0.0.0 Safari/537.36"
25
+ )
26
+
27
+
28
+ class Coinup:
29
+ """CoinUp 永续合约客户端(REST via rnet + WebSocket depth)。
30
+
31
+ 与 CoinW 客户端结构保持一致,差异点:
32
+
33
+ - REST 请求使用 :mod:`rnet`,以规避指纹检测。
34
+ - 仅订单簿 ``book`` 频道依赖 WebSocket,其他数据通过 REST 刷新。
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ client: pybotters.Client,
40
+ *,
41
+ rest_client: rnet.Client | None = None,
42
+ rest_api_common: str | None = None,
43
+ rest_api_futures: str | None = None,
44
+ ws_url: str | None = None,
45
+ security_info: str | None = None,
46
+ exchange_token: str | None = None,
47
+ emulation: rnet.Emulation | rnet.EmulationOption | None = None,
48
+ rest_headers_common: dict[str, str] | None = None,
49
+ rest_headers_futures: dict[str, str] | None = None,
50
+ ) -> None:
51
+ self.client = client
52
+ self.store = CoinUpDataStore()
53
+
54
+ self.rest_api_common = rest_api_common or "https://www.coinup.io/fe-co-api"
55
+ self.rest_api_futures = rest_api_futures or "https://futures.coinup.io/fe-co-api"
56
+ self.ws_url = ws_url or "wss://futuresws.marketketac.com/kline-api/ws"
57
+
58
+ self.security_info = security_info or _DEFAULT_SECURITY_INFO
59
+ self._emulation = emulation or rnet.Emulation.Safari26
60
+
61
+ self._rest_client = rest_client or rnet.Client(
62
+ emulation=self._emulation,
63
+ headers={"User-Agent": _DEFAULT_UA},
64
+ allow_redirects=False,
65
+ history=False,
66
+ )
67
+ self._owns_rest_client = rest_client is None
68
+
69
+ common_headers = {
70
+ "Content-Type": "application/json",
71
+ "Origin": "https://www.coinup.io",
72
+ "Referer": "https://www.coinup.io/",
73
+ "User-Agent": _DEFAULT_UA,
74
+ }
75
+ futures_headers = {
76
+ "Content-Type": "application/json",
77
+ "Origin": "https://futures.coinup.io",
78
+ "Referer": "https://futures.coinup.io/zh_CN/trade",
79
+ "User-Agent": _DEFAULT_UA,
80
+ "exchange-client": "pc",
81
+ "exchange-language": "zh_CN",
82
+ }
83
+ if rest_headers_common:
84
+ common_headers.update(rest_headers_common)
85
+ if rest_headers_futures:
86
+ futures_headers.update(rest_headers_futures)
87
+
88
+ self._exchange_token = exchange_token or self._extract_exchange_token()
89
+ if self._exchange_token:
90
+ futures_headers["exchange-token"] = self._exchange_token
91
+
92
+ self._headers_common = common_headers
93
+ self._headers_futures = futures_headers
94
+
95
+ async def __aenter__(self) -> "Coinup":
96
+ await self.update("detail")
97
+ return self
98
+
99
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
100
+ await self.aclose()
101
+
102
+ async def aclose(self) -> None:
103
+ """Close the underlying rnet client if we created it."""
104
+
105
+ if self._owns_rest_client and hasattr(self._rest_client, "close"):
106
+ await self._rest_client.close()
107
+
108
+ def get_contract_id(self, symbol: str) -> str | None:
109
+ """根据交易对获取合约 ID。"""
110
+
111
+ detail = self.store.detail.get({"symbol": symbol})
112
+ if detail is None:
113
+ return None
114
+ contract_id = detail.get("id")
115
+ if contract_id is None:
116
+ return None
117
+ return str(contract_id)
118
+
119
+ async def update(
120
+ self,
121
+ update_type: Literal[
122
+ "detail",
123
+ "position",
124
+ "balance",
125
+ "orders",
126
+ "history_order",
127
+ "history_orders",
128
+ "all",
129
+ ] = "all",
130
+ *,
131
+ detail_payload: dict[str, Any] | None = None,
132
+ assets_payload: dict[str, Any] | None = None,
133
+ assets_endpoint: Literal["get_assets_list", "wallet_and_unrealized"] = "get_assets_list",
134
+ orders_payload: dict[str, Any] | None = None,
135
+ history_orders_payload: dict[str, Any] | None = None,
136
+ ) -> None:
137
+ """刷新本地缓存,所有 REST 请求通过 rnet 发送。
138
+
139
+ - detail: ``POST /common/public_info`` (公共接口)
140
+ - position/balance: ``POST /position/get_assets_list`` (返回仓位 & 余额)
141
+ - 备选 ``/position/wallet_and_unrealized`` 可通过 ``assets_endpoint`` 指定
142
+ - orders: ``POST /order/current_order_list`` (私有)
143
+ - history_order: ``POST /order/history_order_list`` (私有)
144
+ """
145
+
146
+ include_detail = update_type in {"detail", "all"}
147
+ include_position = update_type in {"position", "all"}
148
+ include_balance = update_type in {"balance", "all"}
149
+ include_orders = update_type in {"orders", "all"}
150
+ include_history_orders = update_type in {
151
+ "history_order",
152
+ "history_orders",
153
+ "all",
154
+ }
155
+
156
+ include_assets = include_position or include_balance
157
+
158
+ if not (include_detail or include_assets or include_orders or include_history_orders):
159
+ raise ValueError(f"Unsupported update_type: {update_type}")
160
+
161
+ tasks: dict[str, asyncio.Task[Any]] = {}
162
+
163
+ if include_detail:
164
+ tasks["detail"] = asyncio.create_task(
165
+ self._fetch_detail(detail_payload)
166
+ )
167
+
168
+ if include_assets:
169
+ tasks["assets"] = asyncio.create_task(
170
+ self._fetch_assets(assets_payload, endpoint=assets_endpoint)
171
+ )
172
+
173
+ if include_orders:
174
+ tasks["orders"] = asyncio.create_task(self._fetch_orders(orders_payload))
175
+
176
+ if include_history_orders:
177
+ tasks["history_orders"] = asyncio.create_task(
178
+ self._fetch_history_orders(history_orders_payload)
179
+ )
180
+
181
+ results: dict[str, Any] = {}
182
+ try:
183
+ for key, task in tasks.items():
184
+ results[key] = await task
185
+ except Exception:
186
+ for task in tasks.values():
187
+ task.cancel()
188
+ raise
189
+
190
+ if include_detail and "detail" in results:
191
+ self.store.detail._onresponse(results["detail"])
192
+
193
+ assets_data = results.get("assets")
194
+ if assets_data is not None:
195
+ if include_position:
196
+ self.store.position._onresponse(assets_data)
197
+ if include_balance:
198
+ self.store.balance._onresponse(assets_data)
199
+
200
+ if include_orders:
201
+ orders_data = results.get("orders")
202
+ if orders_data is not None:
203
+ self.store.orders._onresponse(orders_data)
204
+
205
+ if include_history_orders:
206
+ history_data = results.get("history_orders")
207
+ if history_data is not None:
208
+ self.store.history_orders._onresponse(history_data)
209
+
210
+ async def sub_orderbook(
211
+ self,
212
+ symbols: Sequence[str] | str,
213
+ *,
214
+ depth_step: str = "step0",
215
+ depth_limit: int | None = None,
216
+ ) -> pybotters.ws.WebSocketApp:
217
+ """订阅订单簿深度频道。
218
+
219
+ - ``"e_wlfiusdt"`` -> 发送 ``market_e_wlfiusdt_depth_step0``
220
+ - ``"market_e_wlfiusdt_depth_step0"`` -> 原样发送
221
+ """
222
+
223
+ if isinstance(symbols, str):
224
+ symbols = [symbols]
225
+
226
+ payloads = []
227
+ for symbol in symbols:
228
+ ch = symbol.lower()
229
+ # 去除特殊符号
230
+ ch = ch.replace("-", "").replace("_", "")
231
+ if not ch.startswith("market_"):
232
+ ch = f"market_e_{ch}_depth_{depth_step}"
233
+ payloads.append(
234
+ {
235
+ "event": "sub",
236
+ "params": {
237
+ "channel": ch,
238
+ "cb_id": ch.split("_depth_", 1)[0].replace("market_", ""),
239
+ },
240
+ }
241
+ )
242
+ print(payloads)
243
+
244
+ if not payloads:
245
+ raise ValueError("channels must not be empty")
246
+
247
+ self.store.book.limit = depth_limit
248
+
249
+ ws_headers = {
250
+ "Origin": "https://futures.coinup.io",
251
+ "Referer": "https://futures.coinup.io/zh_CN/trade",
252
+ "User-Agent": _DEFAULT_UA,
253
+ }
254
+
255
+ ws_app = self.client.ws_connect(
256
+ self.ws_url,
257
+ hdlr_bytes=self.store.onmessage,
258
+ headers=ws_headers,
259
+ autoping=False,
260
+ )
261
+ await ws_app._event.wait()
262
+
263
+ for payload in payloads:
264
+ await ws_app.current_ws.send_json(payload)
265
+ await asyncio.sleep(0.05)
266
+
267
+ return ws_app
268
+
269
+ async def place_order(
270
+ self,
271
+ symbol: str,
272
+ *,
273
+ side: Literal["buy", "sell", "BUY", "SELL"],
274
+ volume: float | int | str,
275
+ order_type: Literal["limit", "market", 1, 2] = "limit",
276
+ price: float | int | str | None = None,
277
+ position_type: int | str = 1,
278
+ leverage_level: int | str = 1,
279
+ offset: Literal["open", "close", "OPEN", "CLOSE"] = "open",
280
+ order_unit: int | str = 2,
281
+ trigger_price: float | int | str | None = None,
282
+ is_condition_order: bool = False,
283
+ is_oto: bool = False,
284
+ is_check_liq: int | bool = 1,
285
+ secret: str | None = None,
286
+ take_profit_trigger: float | int | str | None = None,
287
+ take_profit_price: float | int | str | None = 0,
288
+ take_profit_type: int | None = 2,
289
+ stop_loss_trigger: float | int | str | None = None,
290
+ stop_loss_price: float | int | str | None = 0,
291
+ stop_loss_type: int | None = 2,
292
+ extra_params: dict[str, Any] | None = None,
293
+ ) -> dict[str, Any]:
294
+ """
295
+ ``POST /order/order_create`` 下单(默认支持限价/市价)。
296
+ 当 close 时, volume为张数
297
+
298
+ Args:
299
+ symbol: 交易对符号(如 "BTC_USDT"),将自动解析为 contract_id。
300
+ """
301
+ contract_id = self.get_contract_id(symbol)
302
+ if contract_id is None:
303
+ raise ValueError(f"Invalid symbol: {symbol}")
304
+
305
+ normalized_side = self._normalize_side(side)
306
+ normalized_offset = self._normalize_offset(offset)
307
+ order_type_code = self._normalize_order_type(order_type)
308
+
309
+ if order_type_code == 1 and price is None:
310
+ raise ValueError("price is required for CoinUp limit orders")
311
+ price_value = self._format_price(price)
312
+ if order_type_code == 1 and price_value is None:
313
+ raise ValueError("price is required for CoinUp limit orders")
314
+ if price_value is None:
315
+ price_value = 0
316
+
317
+ volume_str = self._format_volume(volume)
318
+ trigger_price_value = self._format_price(trigger_price)
319
+ take_profit_trigger_value = self._format_price(take_profit_trigger)
320
+ take_profit_price_value = self._format_price(take_profit_price)
321
+ stop_loss_trigger_value = self._format_price(stop_loss_trigger)
322
+ stop_loss_price_value = self._format_price(stop_loss_price)
323
+
324
+ payload: dict[str, Any] = {
325
+ "contractId": contract_id,
326
+ "positionType": int(position_type),
327
+ "side": normalized_side,
328
+ "leverageLevel": int(leverage_level),
329
+ "price": price_value,
330
+ "volume": volume_str,
331
+ "triggerPrice": trigger_price_value,
332
+ "open": normalized_offset,
333
+ "type": order_type_code,
334
+ "isConditionOrder": bool(is_condition_order),
335
+ "isOto": bool(is_oto),
336
+ "orderUnit": int(order_unit),
337
+ "isCheckLiq": int(is_check_liq),
338
+ "takerProfitTrigger": take_profit_trigger_value,
339
+ "takerProfitPrice": take_profit_price_value,
340
+ "takerProfitType": take_profit_type,
341
+ "stopLossTrigger": stop_loss_trigger_value,
342
+ "stopLossPrice": stop_loss_price_value,
343
+ "stopLossType": stop_loss_type,
344
+ }
345
+ if extra_params:
346
+ payload.update(extra_params)
347
+
348
+ if secret is None:
349
+ payload["secret"] = self._generate_secret(
350
+ contract_id=str(contract_id),
351
+ leverage_level=str(leverage_level),
352
+ position_type=str(position_type),
353
+ price=price_value,
354
+ side=normalized_side,
355
+ order_type=str(order_type_code),
356
+ volume=volume_str,
357
+ )
358
+ else:
359
+ payload["secret"] = secret
360
+
361
+ body = self._build_payload(payload)
362
+ data = await self._rest_post(
363
+ f"{self.rest_api_futures}/order/order_create",
364
+ body,
365
+ self._headers_futures,
366
+ )
367
+ result = self._ensure_success("place_order", data)
368
+ return result.get("data") or {}
369
+
370
+ async def cancel_order(
371
+ self,
372
+ symbol: str,
373
+ order_id: str | int,
374
+ *,
375
+ is_condition_order: bool = False,
376
+ extra_params: dict[str, Any] | None = None,
377
+ ) -> dict[str, Any] | None:
378
+ """
379
+ ``POST /order/order_cancel`` 取消指定订单。
380
+
381
+ Args:
382
+ symbol: 交易对符号(如 "BTC_USDT"),将自动解析为 contract_id。
383
+ """
384
+ contract_id = self.get_contract_id(symbol)
385
+ if contract_id is None:
386
+ raise ValueError(f"Invalid symbol: {symbol}")
387
+
388
+ payload: dict[str, Any] = {
389
+ "contractId": contract_id,
390
+ "orderId": str(order_id),
391
+ "isConditionOrder": bool(is_condition_order),
392
+ }
393
+ if extra_params:
394
+ payload.update(extra_params)
395
+
396
+ body = self._build_payload(payload)
397
+ data = await self._rest_post(
398
+ f"{self.rest_api_futures}/order/order_cancel",
399
+ body,
400
+ self._headers_futures,
401
+ )
402
+
403
+ result = self._ensure_success("cancel_order", data)
404
+ return result.get("data")
405
+
406
+ def set_exchange_token(self, token: str | None) -> None:
407
+ """Update the ``exchange-token`` header used for CoinUp private REST calls."""
408
+
409
+ self._exchange_token = token
410
+ if token:
411
+ self._headers_futures["exchange-token"] = token
412
+ else:
413
+ self._headers_futures.pop("exchange-token", None)
414
+
415
+ def _build_payload(self, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
416
+ payload = {
417
+ "uaTime": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
418
+ "securityInfo": self.security_info,
419
+ }
420
+ if overrides:
421
+ payload.update(overrides)
422
+ return payload
423
+
424
+ async def _fetch_detail(self, payload: dict[str, Any] | None) -> Any:
425
+ url = f"{self.rest_api_common}/common/public_info"
426
+ data = await self._rest_post(
427
+ url,
428
+ self._build_payload(payload),
429
+ self._headers_common,
430
+ )
431
+ return data
432
+
433
+ async def _fetch_assets(
434
+ self,
435
+ payload: dict[str, Any] | None,
436
+ *,
437
+ endpoint: str,
438
+ ) -> Any:
439
+ url = f"{self.rest_api_futures}/position/{endpoint}"
440
+ data = await self._rest_post(
441
+ url,
442
+ self._build_payload(payload),
443
+ self._headers_futures,
444
+ )
445
+ return data
446
+
447
+ async def _rest_post(
448
+ self,
449
+ url: str,
450
+ payload: dict[str, Any],
451
+ headers: dict[str, str],
452
+ ) -> Any:
453
+ response = await self._rest_client.post(
454
+ url,
455
+ json=payload,
456
+ headers=headers,
457
+ )
458
+ try:
459
+ status = response.status.as_int()
460
+ if status >= 400:
461
+ text = await response.text()
462
+ raise RuntimeError(
463
+ f"CoinUp REST request failed ({status}): {text}"
464
+ )
465
+ return await response.json()
466
+ finally:
467
+ await response.close()
468
+
469
+ async def _fetch_orders(self, payload: dict[str, Any] | None) -> Any:
470
+ url = f"{self.rest_api_futures}/order/current_order_list"
471
+ data = await self._rest_post(
472
+ url,
473
+ self._build_payload(payload or {"contractId": ""}),
474
+ self._headers_futures,
475
+ )
476
+ return data
477
+
478
+ async def _fetch_history_orders(self, payload: dict[str, Any] | None) -> Any:
479
+ url = f"{self.rest_api_futures}/order/history_order_list"
480
+ data = await self._rest_post(
481
+ url,
482
+ self._build_payload(payload or {"contractId": ""}),
483
+ self._headers_futures,
484
+ )
485
+ return data
486
+
487
+ def _extract_exchange_token(self) -> str | None:
488
+ """Best-effort fetch of token from pybotters credential store."""
489
+
490
+ session = getattr(self.client, "_session", None)
491
+ if not session:
492
+ return None
493
+ apis = getattr(session, "__dict__", {}).get("_apis")
494
+ if not isinstance(apis, dict):
495
+ return None
496
+ creds = apis.get("coinup")
497
+ if not creds:
498
+ return None
499
+ token = creds[0]
500
+ return str(token) if token else None
501
+
502
+ @staticmethod
503
+ def _ensure_success(operation: str, payload: Any) -> dict[str, Any]:
504
+ if not isinstance(payload, dict):
505
+ raise RuntimeError(f"{operation} failed: unexpected response {payload}")
506
+ code = payload.get("code")
507
+ succ = payload.get("succ")
508
+ if code is not None and str(code) != "0":
509
+ raise RuntimeError(f"{operation} failed: {payload}")
510
+ if succ is False:
511
+ raise RuntimeError(f"{operation} failed: {payload}")
512
+ return payload
513
+
514
+ @staticmethod
515
+ def _normalize_side(side: str) -> str:
516
+ value = str(side).upper()
517
+ if value not in {"BUY", "SELL"}:
518
+ raise ValueError(f"Unsupported side: {side}")
519
+ return value
520
+
521
+ @staticmethod
522
+ def _normalize_offset(offset: str) -> str:
523
+ value = str(offset).upper()
524
+ mapping = {"OPEN": "OPEN", "CLOSE": "CLOSE"}
525
+ try:
526
+ return mapping[value]
527
+ except KeyError as exc:
528
+ raise ValueError(f"Unsupported offset: {offset}") from exc
529
+
530
+ @staticmethod
531
+ def _normalize_order_type(order_type: Literal["limit", "market", 1, 2]) -> int:
532
+ mapping = {
533
+ "limit": 1,
534
+ "market": 2,
535
+ 1: 1,
536
+ 2: 2,
537
+ }
538
+ try:
539
+ return mapping[order_type] # type: ignore[index]
540
+ except KeyError as exc:
541
+ raise ValueError(f"Unsupported order_type: {order_type}") from exc
542
+
543
+ @staticmethod
544
+ def _generate_secret(
545
+ contract_id: str,
546
+ leverage_level: str,
547
+ position_type: str,
548
+ price: float | int | str,
549
+ side: str,
550
+ order_type: str,
551
+ volume: str,
552
+ ) -> str:
553
+ data = {
554
+ "contractId": contract_id,
555
+ "leverageLevel": leverage_level,
556
+ "positionType": position_type,
557
+ "price": Coinup._stringify_number(price),
558
+ "side": side,
559
+ "type": order_type,
560
+ "volume": Coinup._stringify_number(volume),
561
+ }
562
+ text = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
563
+ raw = f"{_SECRET_PREFIX}{text}"
564
+ return hashlib.md5(raw.encode("utf-8")).hexdigest()
565
+
566
+ @staticmethod
567
+ def _format_price(value: float | int | str | None) -> float | int | str | None:
568
+ if value is None:
569
+ return None
570
+ if isinstance(value, (float, int)):
571
+ return float(value)
572
+ return value
573
+
574
+ @staticmethod
575
+ def _format_volume(value: float | int | str) -> str:
576
+ return str(value)
577
+
578
+ @staticmethod
579
+ def _stringify_number(value: float | int | str | None) -> str:
580
+ if value is None:
581
+ return "0"
582
+ if isinstance(value, str):
583
+ return value
584
+ if isinstance(value, int):
585
+ return str(value)
586
+ if isinstance(value, float):
587
+ if value.is_integer():
588
+ return str(int(value))
589
+ text = format(value, "f").rstrip("0").rstrip(".")
590
+ return text or "0"
591
+ return str(value)
@@ -0,0 +1,334 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from typing import Any, Awaitable, TYPE_CHECKING
7
+ import zlib
8
+
9
+ from aiohttp import ClientResponse
10
+ from pybotters.store import DataStore, DataStoreCollection
11
+
12
+ if TYPE_CHECKING:
13
+ from pybotters.typedefs import Item
14
+ from pybotters.ws import ClientWebSocketResponse
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # {"event_rep":"","channel":"market_e_wlfiusdt_depth_step0","data":null,"tick":{"asks":[[0.1402,9864],[0.1403,23388],[0.1404,9531],[0.1405,4995],[0.1406,3074],[0.1407,18736],[0.1408,3514],[0.1409,6326],[0.141,13217],[0.1411,18253],[0.1412,12214],[0.1413,15243],[0.1414,3606],[0.1415,14894],[0.1416,7932],[0.1417,4973],[0.1418,13031],[0.1419,19793],[0.142,17093],[0.1421,19395],[0.1422,12793],[0.1423,17846],[0.1424,15320],[0.1425,13313],[0.1426,20405],[0.1427,6611],[0.1428,17688],[0.1429,16308],[0.143,10073],[0.1431,15438]],"buys":[[0.1401,9473],[0.14,17486],[0.1399,11957],[0.1398,4824],[0.1397,18447],[0.1396,16929],[0.1395,19859],[0.1394,7283],[0.1393,19609],[0.1392,13638],[0.1391,4146],[0.139,3924],[0.1389,21574],[0.1388,14692],[0.1387,13772],[0.1386,21153],[0.1385,19533],[0.1384,20164],[0.1383,2645],[0.1382,17852],[0.1381,21453],[0.138,19162],[0.1379,17365],[0.1378,9061],[0.1377,14713],[0.1376,12023],[0.1375,11245],[0.1374,9633],[0.1373,5124],[0.1372,5140]]},"ts":1761239630000,"status":"ok"}
19
+
20
+ class Book(DataStore):
21
+
22
+ _KEYS = ["s", "S", "p"]
23
+
24
+ def _init(self) -> None:
25
+ self.limit: int | None = None
26
+
27
+ def _on_message(self, msg: Any) -> None:
28
+ asks = msg["tick"]["asks"]
29
+ bids = msg["tick"]["buys"]
30
+ if self.limit is not None:
31
+ asks = asks[: self.limit]
32
+ bids = bids[: self.limit]
33
+ chanel = msg["channel"]
34
+ symbol = chanel.split("_")[2].upper()
35
+ symbol = symbol.replace("USDT", "-USDT")
36
+ asks = [
37
+ {"s": symbol, "S": "a", "p": float(price), "q": float(quantity)} for price, quantity in asks
38
+ ]
39
+ bids = [
40
+ {"s": symbol, "S": "b", "p": float(price), "q": float(quantity)} for price, quantity in bids
41
+ ]
42
+ self._find_and_delete({"s": symbol})
43
+ self._insert(asks + bids)
44
+
45
+ class Detail(DataStore):
46
+
47
+ _KEYS = ["symbol"]
48
+
49
+ def _onresponse(self, data: Any) -> None:
50
+ data = data.get("data", {})
51
+ clist = data.get("contractList", [])
52
+ # coinResultVo -> marginCoinPrecision 取出来
53
+ for c in clist:
54
+ p = c.get("coinResultVo", {}).get("marginCoinPrecision")
55
+ p = 10 ** (-p)
56
+ c["tick_size"] = p
57
+ c['TickSize'] = p # 兼容用大写开头的字段
58
+ self._update(clist)
59
+
60
+ class Position(DataStore):
61
+
62
+ _KEYS = ["symbol"]
63
+
64
+ def _onresponse(self, data: Any) -> None:
65
+ data = data.get("data", [])
66
+ p_list = data.get("positionList", [])
67
+ self._clear()
68
+ self._update(p_list)
69
+
70
+ class Balance(DataStore):
71
+
72
+ _KEYS = ["symbol"]
73
+
74
+ def _onresponse(self, data: Any) -> None:
75
+ data = data.get("data", [])
76
+ b_list = data.get("accountList", [])
77
+ self._clear()
78
+ self._update(b_list)
79
+
80
+ class Orders(DataStore):
81
+
82
+ _KEYS = ["orderId"]
83
+
84
+ @staticmethod
85
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
86
+ order_id = entry.get("orderId")
87
+ if order_id is None:
88
+ return None
89
+ normalized = dict(entry)
90
+ normalized["orderId"] = str(order_id)
91
+ return normalized
92
+
93
+ def _onresponse(self, data: Any) -> None:
94
+ payload: Any = data
95
+ if isinstance(data, dict):
96
+ payload = data.get("data") or data
97
+ if isinstance(payload, dict):
98
+ payload = payload.get("orderList") or payload.get("orders") or []
99
+
100
+ items: list[dict[str, Any]] = []
101
+ if isinstance(payload, list):
102
+ for entry in payload:
103
+ if not isinstance(entry, dict):
104
+ continue
105
+ normalized = self._normalize(entry)
106
+ if normalized:
107
+ items.append(normalized)
108
+
109
+ self._clear()
110
+ if items:
111
+ self._insert(items)
112
+
113
+ class HistoryOrders(DataStore):
114
+
115
+ _KEYS = ["orderId"]
116
+
117
+ @staticmethod
118
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
119
+ order_id = entry.get("orderId")
120
+ if order_id is None:
121
+ return None
122
+ normalized = dict(entry)
123
+ normalized["orderId"] = str(order_id)
124
+ return normalized
125
+
126
+ def _onresponse(self, data: Any) -> None:
127
+ payload: Any = data
128
+ if isinstance(data, dict):
129
+ payload = data.get("data") or data
130
+ if isinstance(payload, dict):
131
+ payload = payload.get("orderList") or payload.get("orders") or []
132
+
133
+ items: list[dict[str, Any]] = []
134
+ if isinstance(payload, list):
135
+ for entry in payload:
136
+ if not isinstance(entry, dict):
137
+ continue
138
+ normalized = self._normalize(entry)
139
+ if normalized:
140
+ items.append(normalized)
141
+
142
+ self._clear()
143
+ if items:
144
+ self._insert(items)
145
+
146
+ class CoinUpDataStore(DataStoreCollection):
147
+ def _init(self) -> None:
148
+ self._create("book", datastore_class=Book)
149
+ self._create("detail", datastore_class=Detail)
150
+ self._create("position", datastore_class=Position)
151
+ self._create("balance", datastore_class=Balance)
152
+ self._create("orders", datastore_class=Orders)
153
+ self._create("history_orders", datastore_class=HistoryOrders)
154
+
155
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
156
+ decompressed = zlib.decompress(msg, 16 + zlib.MAX_WBITS)
157
+ text = decompressed.decode("utf-8")
158
+ data = json.loads(text)
159
+ chanel = data.get("channel", "")
160
+ if 'depth' in chanel:
161
+ self.book._on_message(data)
162
+
163
+ def onresponse(self, data: Any) -> None:
164
+ pass
165
+
166
+
167
+ @property
168
+ def book(self) -> Book:
169
+ """
170
+ .. code:: json
171
+
172
+ {
173
+ "s": "BTCUSDT",
174
+ "S": "a", # 卖单
175
+ "p": "95640.3",
176
+ "q": "0.807"
177
+ }
178
+
179
+ """
180
+ return self._get("book")
181
+
182
+ @property
183
+ def detail(self) -> Detail:
184
+ """
185
+ .. code:: json
186
+
187
+ {
188
+ "id": 117,
189
+ "contractName": "E-RESOLV-USDT",
190
+ "symbol": "RESOLV-USDT",
191
+ "contractType": "E",
192
+ "coType": "E",
193
+ "contractShowType": "USDT合约",
194
+ "deliveryKind": "0",
195
+ "contractSide": 1,
196
+ "multiplier": 22.8000000000000000,
197
+ "multiplierCoin": "RESOLV",
198
+ "marginCoin": "USDT",
199
+ "originalCoin": "USDT",
200
+ "marginRate": 1.00000000,
201
+ "capitalStartTime": 0,
202
+ "capitalFrequency": 8,
203
+ "settlementFrequency": 1,
204
+ "brokerId": 1,
205
+ "base": "RESOLV",
206
+ "quote": "USDT",
207
+ "coinResultVo": {
208
+ "symbolPricePrecision": 5,
209
+ "depth": [
210
+ "5",
211
+ "4",
212
+ "3"
213
+ ],
214
+ "minOrderVolume": 1,
215
+ "minOrderMoney": 1,
216
+ "maxMarketVolume": 5000000,
217
+ "maxMarketMoney": 6411360,
218
+ "maxLimitVolume": 5000000,
219
+ "maxLimitMoney": 5000000.0000000000000000,
220
+ "priceRange": 0.3000000000,
221
+ "marginCoinPrecision": 4,
222
+ "fundsInStatus": 1,
223
+ "fundsOutStatus": 1
224
+ },
225
+ "sort": 100,
226
+ "maxLever": 75,
227
+ "minLever": 1,
228
+ "contractOtherName": "RESOLV/USDT",
229
+ "subSymbol": "e_resolvusdt",
230
+ "classification": 1,
231
+ "nextCapitalSettTime": 1761292800000,
232
+ "tick_size": 0.0001
233
+ }
234
+ """
235
+ return self._get("detail")
236
+
237
+ @property
238
+ def position(self) -> Position:
239
+ """
240
+ _key: symbol
241
+
242
+ .. code:: json
243
+
244
+ {
245
+ "id": 256538,
246
+ "contractId": 169,
247
+ "contractName": "E-WLFI-USDT",
248
+ "contractOtherName": "WLFI/USDT",
249
+ "symbol": "WLFI-USDT",
250
+ "positionVolume": 1.0,
251
+ "canCloseVolume": 1.0,
252
+ "closeVolume": 0.0,
253
+ "openAvgPrice": 0.1409,
254
+ "indexPrice": 0.14040034,
255
+ "reducePrice": -0.9769279224708908,
256
+ "holdAmount": 16.53718437074,
257
+ "marginRate": 7.852395215348719,
258
+ "realizedAmount": 0.0,
259
+ "returnRate": -0.0177310149041873,
260
+ "orderSide": "BUY",
261
+ "positionType": 1,
262
+ "canUseAmount": 16.11598335074,
263
+ "canSubMarginAmount": 0,
264
+ "openRealizedAmount": -0.0074949,
265
+ "keepRate": 0.015,
266
+ "maxFeeRate": 2.0E-4,
267
+ "unRealizedAmount": -0.0074949,
268
+ "leverageLevel": 5,
269
+ "positionBalance": 2.1060051,
270
+ "tradeFee": "-0.0004",
271
+ "capitalFee": "0",
272
+ "closeProfit": "0",
273
+ "settleProfit": "0",
274
+ "shareAmount": "0",
275
+ "historyRealizedAmount": "-0.0004227",
276
+ "profitRealizedAmount": "-0.0004",
277
+ "openAmount": 0.4227,
278
+ "adlLevel": 2
279
+ }
280
+
281
+ """
282
+ return self._get("position")
283
+
284
+ @property
285
+ def balance(self) -> Balance:
286
+ """
287
+ _key: symbol
288
+
289
+ .. code:: json
290
+
291
+ {
292
+ "symbol": "USDT",
293
+ "originalCoin": "USDT",
294
+ "unRealizedAmount": "-0.0074949",
295
+ "realizedAmount": "0",
296
+ "totalMargin": "16.53718437074",
297
+ "totalAmount": "16.53718437074",
298
+ "canUseAmount": 16.11598335074,
299
+ "availableAmount": 16.11598335074,
300
+ "isolateMargin": "0",
301
+ "walletBalance": "16.54467927074",
302
+ "lockAmount": "0",
303
+ "accountNormal": "16.54467927074",
304
+ "totalMarginRate": "7.8523952153487187"
305
+ }
306
+ """
307
+ return self._get("balance")
308
+
309
+ @property
310
+ def orders(self) -> Orders:
311
+ """
312
+ _key: orderId
313
+
314
+ .. code:: json
315
+
316
+ {
317
+ "orderId": "2951913499074783723",
318
+ "contractId": 169,
319
+ "side": "SELL",
320
+ "price": 0.15,
321
+ "volume": 1,
322
+ "status": 0
323
+ }
324
+ """
325
+ return self._get("orders")
326
+
327
+ @property
328
+ def history_orders(self) -> HistoryOrders:
329
+ """
330
+ _key: orderId
331
+
332
+ 历史委托记录,与 ``orders`` 结构一致。
333
+ """
334
+ return self._get("history_orders")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.89
3
+ Version: 0.92
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
@@ -12,15 +12,17 @@ Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: MIT License
13
13
  Classifier: Programming Language :: Python :: 3
14
14
  Classifier: Topic :: Office/Business :: Financial :: Investment
15
- Requires-Python: >=3.9
15
+ Requires-Python: >=3.11
16
16
  Requires-Dist: aiohttp>=3.10.4
17
17
  Requires-Dist: colorama>=0.4.6
18
18
  Requires-Dist: cryptography>=44.0.2
19
+ Requires-Dist: curl-cffi>=0.13.0
19
20
  Requires-Dist: duckdb>=1.2.2
20
21
  Requires-Dist: numpy>=1.21.0
21
22
  Requires-Dist: pandas>=2.2.3
22
23
  Requires-Dist: pybotters>=1.9.1
23
24
  Requires-Dist: pyecharts>=2.0.8
25
+ Requires-Dist: rnet==3.0.0rc10
24
26
  Description-Content-Type: text/markdown
25
27
 
26
28
  # minquant
@@ -6,6 +6,7 @@ hyperquant/logkit.py,sha256=nUo7nx5eONvK39GOhWwS41zNRL756P2J7-5xGzwXnTY,8462
6
6
  hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
7
7
  hyperquant/broker/auth.py,sha256=xNZEQP0LRRV9BkT2uXBJ-vFfeahUnRVq1bjIT6YbQu8,10089
8
8
  hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
9
+ hyperquant/broker/coinup.py,sha256=I9qWeol1t-0GWUycc8CjOux0K4KlijN3RvPHET0vJFw,20388
9
10
  hyperquant/broker/coinw.py,sha256=SnJU0vASh77rfcpMGWaIfTblQSjQk3vjlW_4juYdbcs,17214
10
11
  hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
11
12
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
@@ -17,6 +18,7 @@ hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb8
17
18
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
18
19
  hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
19
20
  hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
21
+ hyperquant/broker/models/coinup.py,sha256=X_ngB2_sgTOdfAZqTyeWvCN03j-0_inZ6ugZKW6hR7k,11173
20
22
  hyperquant/broker/models/coinw.py,sha256=LvLMVP7i-qkkTK1ubw8eBkMK2RQmFoKPxdKqmC4IToY,22157
21
23
  hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
22
24
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
@@ -26,6 +28,6 @@ hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw
26
28
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
27
29
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
28
30
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
29
- hyperquant-0.89.dist-info/METADATA,sha256=NBPtmJcSeB4mhyzR_K0yISNPcMaiyzLqfjGkA_q6dME,4317
30
- hyperquant-0.89.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
31
- hyperquant-0.89.dist-info/RECORD,,
31
+ hyperquant-0.92.dist-info/METADATA,sha256=g3CE4Sd5ARP0lqmDHAbg693fqAZTBQUU4bn6tGbgpCk,4382
32
+ hyperquant-0.92.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
33
+ hyperquant-0.92.dist-info/RECORD,,