hyperquant 0.59__py3-none-any.whl → 0.61__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,183 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Any, Iterable, Literal
5
+
6
+ import pybotters
7
+
8
+ from .models.edgex import EdgexDataStore
9
+
10
+
11
+ class Edgex:
12
+ """
13
+ Edgex 公共 API (HTTP/WS) 封装。
14
+
15
+ 说明
16
+ - 当前仅包含公共行情数据(不包含私有接口)。
17
+ - 订单簿频道命名规则:``depth.{contractId}.{level}``。
18
+ 成功订阅后,服务器会先推送一次完整快照(depthType=SNAPSHOT),之后持续推送增量(depthType=CHANGED)。
19
+ 解析后的结果存入 ``EdgexDataStore.book``。
20
+
21
+ 参数
22
+ - client: ``pybotters.Client`` 实例
23
+ - api_url: REST 基地址;默认使用 Edgex 官方 testnet 站点
24
+ - ws_url: WebSocket 基地址;如不提供,则默认使用官方文档地址。
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ client: pybotters.Client,
30
+ *,
31
+ api_url: str | None = None,
32
+ ) -> None:
33
+ self.client = client
34
+ self.store = EdgexDataStore()
35
+ # 公共端点可能因环境/地区不同而变化,允许外部覆盖。
36
+ self.api_url = api_url or "https://pro.edgex.exchange"
37
+ self.ws_url = "wss://quote.edgex.exchange"
38
+
39
+ async def __aenter__(self) -> "Edgex":
40
+ # 初始化基础合约元数据,便于后续使用 tickSize 等字段。
41
+ await self.update_detail()
42
+ return self
43
+
44
+ async def __aexit__(
45
+ self,
46
+ exc_type: type[BaseException] | None,
47
+ exc: BaseException | None,
48
+ tb: BaseException | None,
49
+ ) -> None:
50
+ # Edgex 当前没有需要关闭的资源;保持接口与 Ourbit 等类一致。
51
+ return None
52
+
53
+ async def update_detail(self) -> dict[str, Any]:
54
+ """Fetch and cache contract metadata via the public REST endpoint."""
55
+
56
+ url = self._resolve_api_path("/api/v1/public/meta/getMetaData")
57
+ res = await self.client.get(url)
58
+ res.raise_for_status()
59
+ data = await res.json()
60
+
61
+ if data.get("code") != "SUCCESS": # pragma: no cover - defensive guard
62
+ raise RuntimeError(f"Failed to fetch Edgex metadata: {data}")
63
+
64
+ self.store._apply_metadata(data)
65
+ return data
66
+
67
+ def _resolve_api_path(self, path: str) -> str:
68
+ base = (self.api_url or "").rstrip("/")
69
+ return f"{base}{path}"
70
+
71
+ async def sub_orderbook(
72
+ self,
73
+ contract_ids: str | Iterable[str] | None = None,
74
+ *,
75
+ symbols: str | Iterable[str] | None = None,
76
+ level: int = 15,
77
+ ws_url: str | None = None,
78
+ ) -> None:
79
+ """订阅指定合约 ID 或交易对名的订单簿(遵循 Edgex 协议)。
80
+
81
+ 规范
82
+ - 默认 WS 端点:wss://quote.edgex.exchange(可通过参数/实例覆盖)
83
+ - 每个频道的订阅报文:
84
+ {"type": "subscribe", "channel": "depth.{contractId}.{level}"}
85
+ - 服务端在订阅成功后,会先推送一次快照,再持续推送增量。
86
+ """
87
+
88
+ ids: list[str] = []
89
+ if contract_ids is not None:
90
+ if isinstance(contract_ids, str):
91
+ ids.extend([contract_ids])
92
+ else:
93
+ ids.extend(contract_ids)
94
+
95
+ if symbols is not None:
96
+ if isinstance(symbols, str):
97
+ lookup_symbols = [symbols]
98
+ else:
99
+ lookup_symbols = list(symbols)
100
+
101
+ for symbol in lookup_symbols:
102
+ matches = self.store.detail.find({"contractName": symbol})
103
+ if not matches:
104
+ raise ValueError(f"Unknown Edgex symbol: {symbol}")
105
+ ids.append(str(matches[0]["contractId"]))
106
+
107
+ if not ids:
108
+ raise ValueError("contract_ids or symbols must be provided")
109
+
110
+ channels = [f"depth.{cid}.{level}" for cid in ids]
111
+
112
+ # 优先使用参数 ws_url,其次使用实例的 ws_url,最后使用默认地址。
113
+ url = f"{self.ws_url}/api/v1/public/ws?timestamp=" + str(int(time.time() * 1000))
114
+
115
+ # 根据文档:每个频道一条订阅指令,允许一次发送多个订阅对象。
116
+ payload = [{"type": "subscribe", "channel": ch} for ch in channels]
117
+
118
+ wsapp = self.client.ws_connect(url, send_json=payload, hdlr_json=self.store.onmessage)
119
+ # 等待 WS 完成握手再返回,确保订阅报文成功发送。
120
+ await wsapp._event.wait()
121
+
122
+ async def sub_ticker(
123
+ self,
124
+ contract_ids: str | Iterable[str] | None = None,
125
+ *,
126
+ symbols: str | Iterable[str] | None = None,
127
+ all_contracts: bool = False,
128
+ periodic: bool = False,
129
+ ws_url: str | None = None,
130
+ ) -> None:
131
+ """订阅 24 小时行情推送。
132
+
133
+ 参数
134
+ - contract_ids / symbols: 指定单个或多个合约;二者至少提供一个。
135
+ - all_contracts: 订阅 ``ticker.all``(或 ``ticker.all.1s``)。
136
+ - periodic: 与 ``all_contracts`` 配合,true 则订阅 ``ticker.all.1s``。
137
+ """
138
+
139
+ channels: list[str] = []
140
+
141
+ if all_contracts:
142
+ channel = "ticker.all.1s" if periodic else "ticker.all"
143
+ channels.append(channel)
144
+ else:
145
+ ids: list[str] = []
146
+ if contract_ids is not None:
147
+ if isinstance(contract_ids, str):
148
+ ids.append(contract_ids)
149
+ else:
150
+ ids.extend(contract_ids)
151
+
152
+ if symbols is not None:
153
+ if isinstance(symbols, str):
154
+ lookup_symbols = [symbols]
155
+ else:
156
+ lookup_symbols = list(symbols)
157
+
158
+ for symbol in lookup_symbols:
159
+ matches = self.store.detail.find({"contractName": symbol})
160
+ if not matches:
161
+ raise ValueError(f"Unknown Edgex symbol: {symbol}")
162
+ ids.append(str(matches[0]["contractId"]))
163
+
164
+ if not ids:
165
+ raise ValueError("Provide contract_ids/symbols or set all_contracts=True")
166
+
167
+ channels.extend(f"ticker.{cid}" for cid in ids)
168
+
169
+ url = ws_url or f"{self.ws_url}/api/v1/public/ws?timestamp=" + str(int(time.time() * 1000))
170
+ payload = [{"type": "subscribe", "channel": ch} for ch in channels]
171
+
172
+ wsapp = self.client.ws_connect(url, send_json=payload, hdlr_json=self.store.onmessage)
173
+ await wsapp._event.wait()
174
+
175
+
176
+ async def __aexit__(
177
+ self,
178
+ exc_type: type[BaseException] | None,
179
+ exc: BaseException | None,
180
+ tb: BaseException | None,
181
+ ) -> None:
182
+ # Edgex 当前没有需要关闭的资源;保持接口与 Ourbit 等类一致。
183
+ return None
@@ -0,0 +1,518 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, Awaitable, TYPE_CHECKING
5
+
6
+ from aiohttp import ClientResponse
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
+ """Order book data store for the Edgex websocket feed."""
16
+
17
+ _KEYS = ["c", "S", "p"]
18
+
19
+ def _init(self) -> None:
20
+ self._version: int | str | None = None
21
+ self.limit: int | None = None
22
+
23
+ def _on_message(self, msg: dict[str, Any]) -> None:
24
+ content = msg.get("content") or {}
25
+ entries = content.get("data") or []
26
+ data_type = (content.get("dataType") or "").lower()
27
+
28
+ for entry in entries:
29
+ contract_id = entry.get("contractId")
30
+ if contract_id is None:
31
+ continue
32
+
33
+ contract_name = entry.get("contractName")
34
+ end_version = entry.get("endVersion")
35
+ depth_type = (entry.get("depthType") or "").lower()
36
+
37
+ is_snapshot = data_type == "snapshot" or depth_type == "snapshot"
38
+
39
+ if is_snapshot:
40
+ self._handle_snapshot(
41
+ contract_id,
42
+ contract_name,
43
+ entry,
44
+ )
45
+ else:
46
+ self._handle_delta(
47
+ contract_id,
48
+ contract_name,
49
+ entry,
50
+ )
51
+
52
+ if end_version is not None:
53
+ self._version = self._normalize_version(end_version)
54
+
55
+ def _handle_snapshot(
56
+ self,
57
+ contract_id: str,
58
+ contract_name: str | None,
59
+ entry: dict[str, Any],
60
+ ) -> None:
61
+ asks = entry.get("asks") or []
62
+ bids = entry.get("bids") or []
63
+
64
+ self._find_and_delete({"c": contract_id})
65
+
66
+ payload: list[dict[str, Any]] = []
67
+ payload.extend(
68
+ self._build_items(
69
+ contract_id,
70
+ contract_name,
71
+ "a",
72
+ asks,
73
+ )
74
+ )
75
+ payload.extend(
76
+ self._build_items(
77
+ contract_id,
78
+ contract_name,
79
+ "b",
80
+ bids,
81
+ )
82
+ )
83
+
84
+ if payload:
85
+ self._insert(payload)
86
+ self._trim(contract_id, contract_name)
87
+
88
+ def _handle_delta(
89
+ self,
90
+ contract_id: str,
91
+ contract_name: str | None,
92
+ entry: dict[str, Any],
93
+ ) -> None:
94
+ updates: list[dict[str, Any]] = []
95
+ deletes: list[dict[str, Any]] = []
96
+
97
+ asks = entry.get("asks") or []
98
+ bids = entry.get("bids") or []
99
+
100
+ for side, levels in (("a", asks), ("b", bids)):
101
+ for row in levels:
102
+ price, size = self._extract_price_size(row)
103
+ criteria = {"c": contract_id, "S": side, "p": price}
104
+
105
+ if not size or float(size) == 0.0:
106
+ deletes.append(criteria)
107
+ continue
108
+
109
+ updates.append(
110
+ {
111
+ "c": contract_id,
112
+ "S": side,
113
+ "p": price,
114
+ "q": size,
115
+ "s": self._symbol(contract_id, contract_name),
116
+ }
117
+ )
118
+
119
+ if deletes:
120
+ self._delete(deletes)
121
+ if updates:
122
+ self._update(updates)
123
+ self._trim(contract_id, contract_name)
124
+
125
+
126
+ def _build_items(
127
+ self,
128
+ contract_id: str,
129
+ contract_name: str | None,
130
+ side: str,
131
+ rows: list[dict[str, Any]],
132
+ ) -> list[dict[str, Any]]:
133
+ items: list[dict[str, Any]] = []
134
+ for row in rows:
135
+ price, size = self._extract_price_size(row)
136
+ if not size or float(size) == 0.0:
137
+ continue
138
+ items.append(
139
+ {
140
+ "c": contract_id,
141
+ "S": side,
142
+ "p": price,
143
+ "q": size,
144
+ "s": self._symbol(contract_id, contract_name),
145
+ }
146
+ )
147
+ return items
148
+
149
+ @staticmethod
150
+ def _normalize_version(value: Any) -> int | str:
151
+ if value is None:
152
+ return value
153
+ try:
154
+ return int(value)
155
+ except (TypeError, ValueError):
156
+ return str(value)
157
+
158
+ @staticmethod
159
+ def _to_str(value: Any) -> str | None:
160
+ if value is None:
161
+ return None
162
+ return str(value)
163
+
164
+ @staticmethod
165
+ def _extract_price_size(row: dict[str, Any]) -> tuple[str, str]:
166
+ return str(row["price"]), str(row["size"])
167
+
168
+ def _trim(self, contract_id: str, contract_name: str | None) -> None:
169
+ if self.limit is None:
170
+ return
171
+
172
+ query: dict[str, Any]
173
+ symbol = self._symbol(contract_id, contract_name)
174
+ if symbol:
175
+ query = {"s": symbol}
176
+ else:
177
+ query = {"c": contract_id}
178
+
179
+ sort_data = self.sorted(query, self.limit)
180
+ asks = sort_data.get("a", [])
181
+ bids = sort_data.get("b", [])
182
+
183
+ self._find_and_delete(query)
184
+
185
+ trimmed = asks + bids
186
+ if trimmed:
187
+ self._insert(trimmed)
188
+
189
+ @staticmethod
190
+ def _symbol(contract_id: str, contract_name: str | None) -> str:
191
+ if contract_name:
192
+ return str(contract_name)
193
+ return str(contract_id)
194
+
195
+ @property
196
+ def version(self) -> int | str | None:
197
+ """返回当前缓存的订单簿版本号。"""
198
+ return self._version
199
+
200
+ def sorted(
201
+ self,
202
+ query: dict[str, Any] | None = None,
203
+ limit: int | None = None,
204
+ ) -> dict[str, list[dict[str, Any]]]:
205
+ """按买卖方向与价格排序后的订单簿视图。"""
206
+ return self._sorted(
207
+ item_key="S",
208
+ item_asc_key="a",
209
+ item_desc_key="b",
210
+ sort_key="p",
211
+ query=query,
212
+ limit=limit,
213
+ )
214
+
215
+
216
+ class Ticker(DataStore):
217
+ """24 小时行情推送数据。"""
218
+
219
+ _KEYS = ["c"]
220
+
221
+ def _on_message(self, msg: dict[str, Any]) -> None:
222
+ content = msg.get("content") or {}
223
+ entries = content.get("data") or []
224
+ data_type = (content.get("dataType") or "").lower()
225
+
226
+ for entry in entries:
227
+ item = self._format(entry)
228
+ if item is None:
229
+ continue
230
+
231
+ criteria = {"c": item["c"]}
232
+ if data_type == "snapshot":
233
+ self._find_and_delete(criteria)
234
+ self._insert([item])
235
+ else:
236
+ self._update([item])
237
+
238
+ def _format(self, entry: dict[str, Any]) -> dict[str, Any] | None:
239
+ contract_id = entry.get("contractId")
240
+ if contract_id is None:
241
+ return None
242
+
243
+ item: dict[str, Any] = {"c": str(contract_id)}
244
+
245
+ name = entry.get("contractName")
246
+ if name is not None:
247
+ item["s"] = str(name)
248
+
249
+ fields = [
250
+ "priceChange",
251
+ "priceChangePercent",
252
+ "trades",
253
+ "size",
254
+ "value",
255
+ "high",
256
+ "low",
257
+ "open",
258
+ "close",
259
+ "highTime",
260
+ "lowTime",
261
+ "startTime",
262
+ "endTime",
263
+ "lastPrice",
264
+ "indexPrice",
265
+ "oraclePrice",
266
+ "openInterest",
267
+ "fundingRate",
268
+ "fundingTime",
269
+ "nextFundingTime",
270
+ "bestAskPrice",
271
+ "bestBidPrice",
272
+ ]
273
+
274
+ for key in fields:
275
+ value = entry.get(key)
276
+ if value is not None:
277
+ item[key] = str(value)
278
+
279
+ return item
280
+
281
+
282
+ class CoinMeta(DataStore):
283
+ """Coin metadata (precision, StarkEx info, etc.)."""
284
+
285
+ _KEYS = ["coinId"]
286
+
287
+ def _onresponse(self, data: dict[str, Any]) -> None:
288
+ coins = (data.get("data") or {}).get("coinList") or []
289
+ items: list[dict[str, Any]] = []
290
+
291
+ for coin in coins:
292
+ coin_id = coin.get("coinId")
293
+ if coin_id is None:
294
+ continue
295
+ items.append(
296
+ {
297
+ "coinId": str(coin_id),
298
+ "coinName": coin.get("coinName"),
299
+ "stepSize": coin.get("stepSize"),
300
+ "showStepSize": coin.get("showStepSize"),
301
+ "starkExAssetId": coin.get("starkExAssetId"),
302
+ }
303
+ )
304
+
305
+ self._clear()
306
+ if items:
307
+ self._insert(items)
308
+
309
+
310
+ class ContractMeta(DataStore):
311
+ """Per-contract trading parameters from the metadata endpoint."""
312
+
313
+ _KEYS = ["contractId"]
314
+
315
+ _FIELDS = (
316
+ "contractName",
317
+ "baseCoinId",
318
+ "quoteCoinId",
319
+ "tickSize",
320
+ "stepSize",
321
+ "minOrderSize",
322
+ "maxOrderSize",
323
+ "defaultTakerFeeRate",
324
+ "defaultMakerFeeRate",
325
+ "enableTrade",
326
+ "fundingInterestRate",
327
+ "fundingImpactMarginNotional",
328
+ "fundingRateIntervalMin",
329
+ "starkExSyntheticAssetId",
330
+ "starkExResolution",
331
+ )
332
+
333
+ def _onresponse(self, data: dict[str, Any]) -> None:
334
+ contracts = (data.get("data") or {}).get("contractList") or []
335
+ items: list[dict[str, Any]] = []
336
+
337
+ for contract in contracts:
338
+ contract_id = contract.get("contractId")
339
+ if contract_id is None:
340
+ continue
341
+
342
+ payload = {"contractId": str(contract_id)}
343
+ for key in self._FIELDS:
344
+ payload[key] = contract.get(key)
345
+ payload["riskTierList"] = self._simplify_risk_tiers(contract.get("riskTierList"))
346
+
347
+ items.append(payload)
348
+
349
+ self._clear()
350
+ if items:
351
+ self._insert(items)
352
+
353
+ @staticmethod
354
+ def _simplify_risk_tiers(risk_tiers: Any) -> list[dict[str, Any]]:
355
+ items: list[dict[str, Any]] = []
356
+ for tier in risk_tiers or []:
357
+ items.append(
358
+ {
359
+ "tier": tier.get("tier"),
360
+ "positionValueUpperBound": tier.get("positionValueUpperBound"),
361
+ "maxLeverage": tier.get("maxLeverage"),
362
+ "maintenanceMarginRate": tier.get("maintenanceMarginRate"),
363
+ "starkExRisk": tier.get("starkExRisk"),
364
+ "starkExUpperBound": tier.get("starkExUpperBound"),
365
+ }
366
+ )
367
+ return items
368
+
369
+ class EdgexDataStore(DataStoreCollection):
370
+ """Edgex DataStore collection exposing the order book feed."""
371
+
372
+ def _init(self) -> None:
373
+ self._create("book", datastore_class=Book)
374
+ self._create("ticker", datastore_class=Ticker)
375
+ self._create("meta_coin", datastore_class=CoinMeta)
376
+ self._create("detail", datastore_class=ContractMeta)
377
+
378
+ @property
379
+ def book(self) -> Book:
380
+ """
381
+ 获取 Edgex 合约订单簿数据流。
382
+
383
+ .. code:: json
384
+
385
+ [
386
+ {
387
+ "c": "10000001", # 合约 ID
388
+ "s": "BTCUSD",
389
+ "S": "a", # 方向 a=卖 b=买
390
+ "p": "117388.2", # 价格
391
+ "q": "12.230", # 数量
392
+ }
393
+ ]
394
+ """
395
+ return self._get("book")
396
+
397
+
398
+
399
+ @property
400
+ def coins(self) -> CoinMeta:
401
+ """
402
+ 获取币种精度及 StarkEx 资产信息列表。
403
+
404
+ .. code:: json
405
+
406
+ [
407
+ {
408
+ "coinId": "1000",
409
+ "coinName": "USDT",
410
+ "stepSize": "0.000001",
411
+ "showStepSize": "0.0001",
412
+ "starkExAssetId": "0x33bda5c9..."
413
+ }
414
+ ]
415
+ """
416
+ return self._get("meta_coin")
417
+
418
+ @property
419
+ def detail(self) -> ContractMeta:
420
+ """
421
+ 获取合约级别的交易参数。
422
+
423
+ .. code:: json
424
+
425
+ [
426
+ {
427
+ "contractId": "10000001",
428
+ "contractName": "BTCUSDT",
429
+ "baseCoinId": "1001",
430
+ "quoteCoinId": "1000",
431
+ "tickSize": "0.1",
432
+ "stepSize": "0.001",
433
+ "minOrderSize": "0.001",
434
+ "maxOrderSize": "50.000",
435
+ "defaultMakerFeeRate": "0.0002",
436
+ "defaultTakerFeeRate": "0.00055",
437
+ "enableTrade": true,
438
+ "fundingInterestRate": "0.0003",
439
+ "fundingImpactMarginNotional": "10",
440
+ "fundingRateIntervalMin": "240",
441
+ "starkExSyntheticAssetId": "0x42544332...",
442
+ "starkExResolution": "0x2540be400",
443
+ "riskTierList": [
444
+ {
445
+ "tier": 1,
446
+ "positionValueUpperBound": "50000",
447
+ "maxLeverage": "100",
448
+ "maintenanceMarginRate": "0.005",
449
+ "starkExRisk": "21474837",
450
+ "starkExUpperBound": "214748364800000000000"
451
+ }
452
+ ]
453
+ }
454
+ ]
455
+ """
456
+ return self._get("detail")
457
+
458
+ @property
459
+ def ticker(self) -> Ticker:
460
+ """
461
+ 获取 24 小时行情推送。
462
+
463
+ .. code:: json
464
+
465
+ [
466
+ {
467
+ "c": "10000001", # 合约 ID
468
+ "s": "BTCUSD", # 合约名称
469
+ "lastPrice": "117400", # 最新价
470
+ "priceChange": "200", # 涨跌额
471
+ "priceChangePercent": "0.0172", # 涨跌幅
472
+ "size": "1250", # 24h 成交量
473
+ "value": "147000000", # 24h 成交额
474
+ "high": "118000", # 24h 最高价
475
+ "low": "116500", # 低价
476
+ "open": "116800", # 开盘价
477
+ "close": "117400", # 收盘价
478
+ "indexPrice": "117350", # 指数价
479
+ "oraclePrice": "117360.12", # 预言机价
480
+ "openInterest": "50000", # 持仓量
481
+ "fundingRate": "0.000234", # 当前资金费率
482
+ "fundingTime": "1758240000000", # 上一次结算时间
483
+ "nextFundingTime": "1758254400000", # 下一次结算时间
484
+ "bestAskPrice": "117410", # 卖一价
485
+ "bestBidPrice": "117400" # 买一价
486
+ }
487
+ ]
488
+ """
489
+ return self._get("ticker")
490
+
491
+ async def initialize(self, *aws: Awaitable["ClientResponse"]) -> None:
492
+ """Populate metadata stores from awaited HTTP responses."""
493
+
494
+ for fut in asyncio.as_completed(aws):
495
+ res = await fut
496
+ data = await res.json()
497
+ if res.url.path == "/api/v1/public/meta/getMetaData":
498
+ self._apply_metadata(data)
499
+
500
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
501
+ channel = (msg.get("channel") or "").lower()
502
+ msg_type = (msg.get("type") or "").lower()
503
+
504
+ if msg_type == "ping" and ws is not None:
505
+ payload = {"type": "pong", "time": msg.get("time")}
506
+ asyncio.create_task(ws.send_json(payload))
507
+ return
508
+
509
+ if "depth" in channel and msg_type in {"quote-event", "payload"}:
510
+ self.book._on_message(msg)
511
+
512
+ if channel.startswith("ticker") and msg_type in {"payload", "quote-event"}:
513
+ self.ticker._on_message(msg)
514
+
515
+
516
+ def _apply_metadata(self, data: dict[str, Any]) -> None:
517
+ self.coins._onresponse(data)
518
+ self.detail._onresponse(data)
@@ -80,7 +80,6 @@ class Book(DataStore):
80
80
  """返回最后更新时间"""
81
81
  return self._time
82
82
 
83
- @property
84
83
  def sorted(
85
84
  self, query: Item | None = None, limit: int | None = None
86
85
  ) -> dict[str, list[Item]]:
hyperquant/broker/ws.py CHANGED
@@ -1,4 +1,6 @@
1
1
  import asyncio
2
+ import time
3
+
2
4
  import pybotters
3
5
  from pybotters.ws import ClientWebSocketResponse, logger
4
6
  from pybotters.auth import Hosts
@@ -17,8 +19,16 @@ class Heartbeat:
17
19
  await ws.send_str('{"method":"ping"}')
18
20
  await asyncio.sleep(10.0)
19
21
 
22
+ @staticmethod
23
+ async def edgex(ws: pybotters.ws.ClientWebSocketResponse):
24
+ while not ws.closed:
25
+ now = str(int(time.time() * 1000))
26
+ await ws.send_json({"type": "ping", "time": now})
27
+ await asyncio.sleep(20.0)
28
+
20
29
  pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
21
30
  pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
31
+ pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
22
32
 
23
33
  class WssAuth:
24
34
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.59
3
+ Version: 0.61
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
@@ -5,17 +5,19 @@ 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
7
  hyperquant/broker/auth.py,sha256=oA9Yw1I59-u0Tnoj2e4wUup5q8V5T2qpga5RKbiAiZI,2614
8
+ hyperquant/broker/edgex.py,sha256=qQtc8jZqB5ZODoGGVcG_aIVUlrJX_pRF9EyO927LiVM,6646
8
9
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
9
10
  hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
10
- hyperquant/broker/ws.py,sha256=umRzxwCaZaRIgIq4YY-AuA0wCXFT0uOBmQbIXFY8CK0,1555
11
+ hyperquant/broker/ws.py,sha256=Ce5-g2BWhS8ZmVoLjtSj3Eb1TNIN4pgEIyJ0ODjiwUo,1902
11
12
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
12
13
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
14
+ hyperquant/broker/models/edgex.py,sha256=texNBwz_9r9CTl7dRNjvSm_toxV_og0TWnap3dqUk2s,15795
13
15
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
14
- hyperquant/broker/models/ourbit.py,sha256=6j6wc2QQsn25hpRk_yVGJm-v_YyiHh-Yq6VNWt39mkY,41979
16
+ hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
15
17
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
16
18
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
17
19
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
18
20
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
19
- hyperquant-0.59.dist-info/METADATA,sha256=2AsJ7yBJN3HSrPXOAOmTOJNWAW2-7K0G-Xj5-26Hci0,4317
20
- hyperquant-0.59.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- hyperquant-0.59.dist-info/RECORD,,
21
+ hyperquant-0.61.dist-info/METADATA,sha256=R8ITR5IGZHsDFVEG982ItyBnYhMolLcy8ZZxGcptE4w,4317
22
+ hyperquant-0.61.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ hyperquant-0.61.dist-info/RECORD,,