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.
- hyperquant/broker/edgex.py +183 -0
- hyperquant/broker/models/edgex.py +518 -0
- hyperquant/broker/models/ourbit.py +0 -1
- hyperquant/broker/ws.py +10 -0
- {hyperquant-0.59.dist-info → hyperquant-0.61.dist-info}/METADATA +1 -1
- {hyperquant-0.59.dist-info → hyperquant-0.61.dist-info}/RECORD +7 -5
- {hyperquant-0.59.dist-info → hyperquant-0.61.dist-info}/WHEEL +0 -0
@@ -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)
|
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.
|
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=
|
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=
|
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.
|
20
|
-
hyperquant-0.
|
21
|
-
hyperquant-0.
|
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,,
|
File without changes
|