hyperquant 0.93__py3-none-any.whl → 0.94__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,487 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import TYPE_CHECKING, Any, Literal, Sequence
5
+
6
+ from pybotters.store import DataStore, DataStoreCollection
7
+
8
+ if TYPE_CHECKING:
9
+ from pybotters.typedefs import Item
10
+ from pybotters.ws import ClientWebSocketResponse
11
+
12
+
13
+ def _maybe_to_dict(payload: Any) -> Any:
14
+ if payload is None:
15
+ return None
16
+ if hasattr(payload, "to_dict"):
17
+ return payload.to_dict()
18
+ if hasattr(payload, "model_dump"):
19
+ return payload.model_dump()
20
+ return payload
21
+
22
+
23
+ class Book(DataStore):
24
+ """Bitmart 合约深度数据。"""
25
+
26
+ _KEYS = ["s", "S", "p"]
27
+
28
+ def _init(self) -> None:
29
+ self.limit: int | None = None
30
+ self.id_to_symbol: dict[str, str] = {}
31
+ self._state: dict[str, dict[str, dict[float, float]]] = {}
32
+ self._last_update: float = 0.0
33
+
34
+ @classmethod
35
+ def _make_entry(cls, symbol: str, side: Literal["a", "b"], price: str, size: str) -> dict[str, Any]:
36
+ return {
37
+ "s": symbol,
38
+ "S": side,
39
+ "p": price,
40
+ "q": size,
41
+ }
42
+
43
+ def _on_message(self, msg: dict[str, Any]) -> None:
44
+ data = msg.get("data")
45
+ group = msg.get("group")
46
+ if not isinstance(data, dict) or not isinstance(group, str):
47
+ return
48
+
49
+ try:
50
+ _, contract_id = group.split(":", 1)
51
+ except ValueError:
52
+ return
53
+
54
+ symbol = self.id_to_symbol.get(contract_id) or contract_id
55
+
56
+ state = self._state.setdefault(symbol, {"a": {}, "b": {}})
57
+
58
+ way = data.get("way")
59
+ depths = data.get("depths") or []
60
+ if way not in {1, 2} or not isinstance(depths, Sequence):
61
+ return
62
+
63
+ side_key = "b" if way == 1 else "a"
64
+ if self.limit:
65
+ depths = depths[: self.limit]
66
+
67
+ self._find_and_delete({'s': symbol, 'S': side_key})
68
+ self._update([
69
+ self._make_entry(
70
+ symbol,
71
+ side_key,
72
+ entry.get("price", '0'),
73
+ entry.get("vol", '0'),
74
+ )
75
+ for entry in depths
76
+ ])
77
+
78
+ self._last_update = time.time()
79
+
80
+ def sorted(self, query: Item | None = None, limit: int | None = None) -> dict[str, list[Item]]:
81
+ return self._sorted(
82
+ item_key="S",
83
+ item_asc_key="a",
84
+ item_desc_key="b",
85
+ sort_key="p",
86
+ query=query,
87
+ limit=limit,
88
+ )
89
+
90
+ @property
91
+ def last_update(self) -> float:
92
+ return self._last_update
93
+
94
+
95
+ class Detail(DataStore):
96
+ """Bitmart 合约详情。"""
97
+
98
+ _KEYS = ["name"]
99
+
100
+ def _onresponse(self, data: Any) -> None:
101
+ payload = _maybe_to_dict(data) or {}
102
+ contracts = payload.get("data", {}).get("contracts") or []
103
+
104
+ records: list[dict[str, Any]] = []
105
+ for item in contracts:
106
+ if not isinstance(item, dict):
107
+ continue
108
+ record: dict[str, Any] = {}
109
+ contract = item.get("contract") or {}
110
+ if isinstance(contract, dict):
111
+ record.update(contract)
112
+ risk_limit = item.get("risk_limit") or {}
113
+ if isinstance(risk_limit, dict):
114
+ record.update({f"risk_{k}": v for k, v in risk_limit.items()})
115
+ fee_config = item.get("fee_config") or {}
116
+ if isinstance(fee_config, dict):
117
+ record.update({f"fee_{k}": v for k, v in fee_config.items()})
118
+ plan_order_config = item.get("plan_order_config") or {}
119
+ if isinstance(plan_order_config, dict):
120
+ record.update({f"plan_{k}": v for k, v in plan_order_config.items()})
121
+ tag_detail = item.get("contract_tag_detail") or {}
122
+ if isinstance(tag_detail, dict):
123
+ record.update({f"tag_{k}": v for k, v in tag_detail.items()})
124
+
125
+ contract_id = record.get("contract_id") or contract.get("contract_id")
126
+ if contract_id is None:
127
+ continue
128
+ record["contract_id"] = contract_id
129
+ record["tick_size"] = record.get("price_unit")
130
+ records.append(record)
131
+
132
+ self._clear()
133
+ if records:
134
+ self._insert(records)
135
+
136
+
137
+ class Orders(DataStore):
138
+ """订单列表。"""
139
+
140
+ _KEYS = ["order_id"]
141
+
142
+ @staticmethod
143
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
144
+ order_id = entry.get("order_id")
145
+ if order_id is None:
146
+ return None
147
+ normalized = dict(entry)
148
+ normalized["order_id"] = order_id
149
+ return normalized
150
+
151
+ def _onresponse(self, data: Any) -> None:
152
+ payload = _maybe_to_dict(data) or {}
153
+ orders = payload.get("data", {}).get("orders") or []
154
+ items: list[dict[str, Any]] = []
155
+ for entry in orders:
156
+ if not isinstance(entry, dict):
157
+ continue
158
+ normalized = self._normalize(entry)
159
+ if normalized:
160
+ items.append(normalized)
161
+ self._clear()
162
+ if items:
163
+ self._insert(items)
164
+
165
+
166
+ class Positions(DataStore):
167
+ """持仓信息。"""
168
+
169
+ _KEYS = ["position_id"]
170
+
171
+ @staticmethod
172
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
173
+ position_id = entry.get("position_id")
174
+ if position_id is None:
175
+ return None
176
+ normalized = dict(entry)
177
+ normalized["position_id"] = position_id
178
+ return normalized
179
+
180
+ def _onresponse(self, data: Any) -> None:
181
+ payload = _maybe_to_dict(data) or {}
182
+ positions = payload.get("data", {}).get("positions") or []
183
+ items: list[dict[str, Any]] = []
184
+ for entry in positions:
185
+ if not isinstance(entry, dict):
186
+ continue
187
+ normalized = self._normalize(entry)
188
+ if normalized:
189
+ items.append(normalized)
190
+ self._clear()
191
+ if items:
192
+ self._insert(items)
193
+
194
+
195
+ class Balances(DataStore):
196
+ """账户资产信息。"""
197
+
198
+ _KEYS = ["coin_code"]
199
+
200
+ @staticmethod
201
+ def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
202
+ coin = entry.get("coin_code")
203
+ if coin is None:
204
+ return None
205
+ normalized = dict(entry)
206
+ normalized["coin_code"] = coin
207
+ return normalized
208
+
209
+ def _onresponse(self, data: Any) -> None:
210
+ payload = _maybe_to_dict(data) or {}
211
+ assets = payload.get("data", {}).get("assets") or []
212
+ items: list[dict[str, Any]] = []
213
+ for entry in assets:
214
+ if not isinstance(entry, dict):
215
+ continue
216
+ normalized = self._normalize(entry)
217
+ if normalized:
218
+ items.append(normalized)
219
+ self._clear()
220
+ if items:
221
+ self._insert(items)
222
+
223
+
224
+ class BitmartDataStore(DataStoreCollection):
225
+ """Bitmart 合约数据集合。"""
226
+
227
+ def _init(self) -> None:
228
+ self._create("book", datastore_class=Book)
229
+ self._create("detail", datastore_class=Detail)
230
+ self._create("orders", datastore_class=Orders)
231
+ self._create("positions", datastore_class=Positions)
232
+ self._create("balances", datastore_class=Balances)
233
+
234
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
235
+ if isinstance(msg, dict):
236
+ group = msg.get("group")
237
+ if isinstance(group, str) and group.startswith("Depth"):
238
+ self.book._on_message(msg)
239
+
240
+ @property
241
+ def book(self) -> Book:
242
+ """
243
+ .. code:: json
244
+
245
+ {
246
+ "s": "BTCUSDT",
247
+ "S": "a", # 卖单
248
+ "p": "95640.3",
249
+ "q": "0.807"
250
+ }
251
+ """
252
+ return self._get("book")
253
+
254
+ @property
255
+ def detail(self) -> Detail:
256
+ """`ifcontract/contracts_all` 返回的合约配置信息。
257
+
258
+ .. code:: json
259
+
260
+ {
261
+ "contract_id": 1,
262
+ "index_id": 1,
263
+ "name": "BTCUSDT",
264
+ "display_name": "BTCUSDT 永续合约",
265
+ "display_name_en": "BTCUSDT_SWAP",
266
+ "contract_type": 1,
267
+ "base_coin": "BTC",
268
+ "quote_coin": "USDT",
269
+ "price_coin": "BTC",
270
+ "exchange": "*",
271
+ "contract_size": "0.001",
272
+ "begin_at": "2022-02-27T16:00:00Z",
273
+ "end_at": "2020-01-01T00:00:00Z",
274
+ "delive_at": "2018-10-01T02:00:00Z",
275
+ "delivery_cycle": 28800,
276
+ "min_leverage": "1",
277
+ "max_leverage": "200",
278
+ "price_unit": "0.1",
279
+ "tick_size": "0.1",
280
+ "vol_unit": "1",
281
+ "value_unit": "0.1",
282
+ "min_vol": "1",
283
+ "max_vol": "500000",
284
+ "liquidation_warn_ratio": "0.85",
285
+ "fast_liquidation_ratio": "0.8",
286
+ "settle_type": 1,
287
+ "open_type": 3,
288
+ "compensate_type": 1,
289
+ "status": 3,
290
+ "block": 1,
291
+ "rank": 1,
292
+ "created_at": "2018-07-12T09:16:57Z",
293
+ "depth_bord": "0.0375",
294
+ "base_coin_zh": "",
295
+ "base_coin_en": "",
296
+ "max_rate": "0.0375",
297
+ "min_rate": "-0.0375",
298
+ "market_status": 0,
299
+ "hedge_name": "binance",
300
+ "icon_url": "/static-file/public/coin/BTC-20200604060942.png",
301
+ "robot_risk_threshold": "0",
302
+ "fund_rate_threshold": "0",
303
+ "fund_rate_switch": 0,
304
+ "fund_rate_config": "0",
305
+ "market_price_rate": "0.003",
306
+ "robot_fund_rate_offset": "0",
307
+ "credit_max_leverage": 20,
308
+ "limit_ratio": "0.05",
309
+ "max_order_num": 200,
310
+ "min_trade_val": "5",
311
+ "bind_order_flag": false,
312
+ "market_max_vol": "80000",
313
+ "quote_type": 1,
314
+ "risk_contract_id": 1,
315
+ "risk_base_limit": "1000000",
316
+ "risk_step": "1000000",
317
+ "risk_maintenance_margin": "0.0025",
318
+ "risk_initial_margin": "0.005",
319
+ "risk_status": 1,
320
+ "fee_contract_id": 1,
321
+ "fee_maker_fee": "0.0002",
322
+ "fee_taker_fee": "0.0006",
323
+ "fee_settlement_fee": "0",
324
+ "fee_created_at": "2018-07-12T20:47:22Z",
325
+ "plan_contract_id": 0,
326
+ "plan_min_scope": "0.001",
327
+ "plan_max_scope": "2",
328
+ "plan_max_count": 100,
329
+ "plan_min_life_cycle": 24,
330
+ "plan_max_life_cycle": 438000,
331
+ "tag_tag_id": 1,
332
+ "tag_tag_name": "hot"
333
+ }
334
+ """
335
+ return self._get("detail")
336
+
337
+ @property
338
+ def orders(self) -> Orders:
339
+ """用户订单 (`userAllOrders`)。
340
+
341
+ .. code:: json
342
+
343
+ [
344
+ {
345
+ "order_id": 3000236525013551,
346
+ "contract_id": 72,
347
+ "position_id": 0,
348
+ "account_id": 2008001004625862,
349
+ "price": "0.25",
350
+ "vol": "1",
351
+ "done_vol": "0",
352
+ "done_avg_price": "0",
353
+ "way": 1,
354
+ "category": 1,
355
+ "make_fee": "0",
356
+ "take_fee": "0",
357
+ "origin": "web",
358
+ "created_at": "2025-10-29T08:23:20.745717Z",
359
+ "updated_at": "2025-10-29T08:23:20.753482Z",
360
+ "finished_at": "",
361
+ "status": 2,
362
+ "errno": 0,
363
+ "mode": 1,
364
+ "leverage": "10",
365
+ "open_type": 2,
366
+ "order_type": 0,
367
+ "extends": {
368
+ "remark": "default",
369
+ "broker_id": "",
370
+ "order_type": 0,
371
+ "bonus_only": false,
372
+ "request_trace_id": "",
373
+ "trigger_ratio_type": 0,
374
+ "is_guaranteed_sl_or_tp": false,
375
+ "is_market_zero_slippage": false,
376
+ "zero_slippage_ratio": ""
377
+ },
378
+ "client_order_id": "",
379
+ "executive_price": "",
380
+ "life_cycle": 0,
381
+ "price_type": 0,
382
+ "price_way": 0,
383
+ "plan_category": 0,
384
+ "activation_price": "",
385
+ "callback_rate": "",
386
+ "executive_order_id": 0,
387
+ "bind_leverage": "",
388
+ "pre_plan_order_id": 0,
389
+ "stop_profit_executive_price": "",
390
+ "stop_profit_price_type": 0,
391
+ "stop_loss_executive_price": "",
392
+ "stop_loss_price_type": 0,
393
+ "liquidation_fee": "",
394
+ "account_type": 0,
395
+ "pnl": "",
396
+ "data_type": "",
397
+ "position_mode": 1,
398
+ "pnl_rate": "",
399
+ "preset_is_guaranteed_sl": false,
400
+ "preset_is_guaranteed_tp": false
401
+ }
402
+ ]
403
+ """
404
+ return self._get("orders")
405
+
406
+ @property
407
+ def positions(self) -> Positions:
408
+ """用户持仓 (`userPositions`)。
409
+
410
+ .. code:: json
411
+
412
+ [
413
+ {
414
+ "position_id": 3000236533088511,
415
+ "account_id": 2008001004625862,
416
+ "contract_id": 72,
417
+ "hold_vol": "1",
418
+ "freeze_vol": "0",
419
+ "close_vol": "0",
420
+ "hold_avg_price": "0.2964901",
421
+ "open_avg_price": "0.2964901",
422
+ "close_avg_price": "0",
423
+ "oim": "0.02982690406",
424
+ "im": "0.02982690406",
425
+ "mm": "0.000741261625",
426
+ "realised_profit": "-0.00017789406",
427
+ "earnings": "-0.00017789406",
428
+ "hold_fee": "0",
429
+ "open_type": 2,
430
+ "position_type": 1,
431
+ "status": 1,
432
+ "errno": 0,
433
+ "created_at": "2025-10-29T11:16:37.63704Z",
434
+ "updated_at": "2025-10-29T11:16:37.63704Z",
435
+ "notional_value": "0.2964901",
436
+ "fair_value": "0.29650465",
437
+ "current_value": "0.2965151",
438
+ "liquidation_value": "-10.702412850255",
439
+ "bankruptcy_value": "0",
440
+ "close_able_vol": "1",
441
+ "bankruptcy_fee": "0.00017789406",
442
+ "current_un_earnings": "0.000025",
443
+ "fair_un_earnings": "0.00001455",
444
+ "liquidate_price": "0",
445
+ "current_roe": "0.0008431570971989815",
446
+ "fair_roe": "0.0004907174305698073",
447
+ "current_notional_roe": "0.0008431984744178642",
448
+ "fair_notional_roe": "0.000490741512111197",
449
+ "leverage": "0.0269540457075690273",
450
+ "bind_leverage": "10",
451
+ "account_type": 0,
452
+ "position_mode": 0,
453
+ "fee": "-0.00017789406"
454
+ }
455
+ ]
456
+ """
457
+ return self._get("positions")
458
+
459
+ @property
460
+ def balances(self) -> Balances:
461
+ """账户资产 (`copy/trade/user/info`)。
462
+
463
+ .. code:: json
464
+
465
+ [
466
+ {
467
+ "account_id": 14794011,
468
+ "coin_code": "USDT",
469
+ "available_vol": "0",
470
+ "cash_vol": "0",
471
+ "freeze_vol": "0",
472
+ "realised_vol": "0",
473
+ "un_realised_vol": "0",
474
+ "earnings_vol": "0",
475
+ "total_im": "",
476
+ "margin_balance": "",
477
+ "available_balance": "",
478
+ "trans_out_balance": "",
479
+ "status": 0,
480
+ "total_balance": "",
481
+ "account_rights": "0",
482
+ "bonus_voucher_vol": "",
483
+ "freeze_bonus_voucher_vol": ""
484
+ }
485
+ ]
486
+ """
487
+ return self._get("balances")