hyperquant 0.59__py3-none-any.whl → 0.62__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/lbank.py +103 -0
- hyperquant/broker/models/edgex.py +518 -0
- hyperquant/broker/models/lbank.py +222 -0
- hyperquant/broker/models/ourbit.py +0 -1
- hyperquant/broker/ws.py +10 -0
- {hyperquant-0.59.dist-info → hyperquant-0.62.dist-info}/METADATA +1 -1
- {hyperquant-0.59.dist-info → hyperquant-0.62.dist-info}/RECORD +9 -5
- {hyperquant-0.59.dist-info → hyperquant-0.62.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,103 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import itertools
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import time
|
8
|
+
import zlib
|
9
|
+
from typing import Iterable, Literal
|
10
|
+
|
11
|
+
import pybotters
|
12
|
+
|
13
|
+
from .models.lbank import LbankDataStore
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
# https://ccapi.rerrkvifj.com 似乎是spot的api
|
18
|
+
# https://uuapi.rerrkvifj.com 似乎是合约的api
|
19
|
+
|
20
|
+
|
21
|
+
class Lbank:
|
22
|
+
"""LBank public market-data client (REST + WS)."""
|
23
|
+
|
24
|
+
def __init__(
|
25
|
+
self,
|
26
|
+
client: pybotters.Client,
|
27
|
+
*,
|
28
|
+
front_api: str | None = None,
|
29
|
+
rest_api: str | None = None,
|
30
|
+
ws_url: str | None = None,
|
31
|
+
) -> None:
|
32
|
+
self.client = client
|
33
|
+
self.store = LbankDataStore()
|
34
|
+
self.front_api = front_api or "https://uuapi.rerrkvifj.com"
|
35
|
+
self.rest_api = rest_api or "https://api.lbkex.com"
|
36
|
+
self.ws_url = ws_url or "wss://uuws.rerrkvifj.com/ws/v3"
|
37
|
+
self._req_id = itertools.count(int(time.time() * 1000))
|
38
|
+
self._ws_app = None
|
39
|
+
|
40
|
+
async def __aenter__(self) -> "Lbank":
|
41
|
+
await self.update("detail")
|
42
|
+
return self
|
43
|
+
|
44
|
+
async def __aexit__(self, exc_type, exc, tb) -> None:
|
45
|
+
pass
|
46
|
+
|
47
|
+
async def update(self, update_type: Literal["detail", "ticker", "all"]) -> list[dict]:
|
48
|
+
all_urls = [f"{self.front_api}/cfd/agg/v1/instrument"]
|
49
|
+
url_map = {"detail": [all_urls[0]], "all": all_urls}
|
50
|
+
|
51
|
+
|
52
|
+
try:
|
53
|
+
urls = url_map[update_type]
|
54
|
+
except KeyError:
|
55
|
+
raise ValueError(f"update_type err: {update_type}")
|
56
|
+
|
57
|
+
# await self.store.initialize(*(self.client.get(url) for url in urls))
|
58
|
+
if update_type == "detail" or update_type == "all":
|
59
|
+
await self.store.initialize(
|
60
|
+
self.client.post(
|
61
|
+
all_urls[0],
|
62
|
+
json={"ProductGroup": "SwapU"},
|
63
|
+
headers={"source": "4", "versionflage": "true"},
|
64
|
+
)
|
65
|
+
)
|
66
|
+
|
67
|
+
|
68
|
+
async def sub_orderbook(self, symbols: list[str], limit: int | None = None) -> None:
|
69
|
+
"""订阅指定交易对的订单簿(遵循 LBank 协议)。
|
70
|
+
"""
|
71
|
+
|
72
|
+
send_jsons = []
|
73
|
+
y = 3000000001
|
74
|
+
if limit:
|
75
|
+
self.store.book.limit = limit
|
76
|
+
|
77
|
+
for symbol in symbols:
|
78
|
+
|
79
|
+
info = self.store.detail.get({"symbol": symbol})
|
80
|
+
if not info:
|
81
|
+
raise ValueError(f"Unknown LBank symbol: {symbol}")
|
82
|
+
|
83
|
+
tick_size = info['tick_size']
|
84
|
+
sub_i = symbol + "_" + str(tick_size) + "_25"
|
85
|
+
send_jsons.append(
|
86
|
+
{
|
87
|
+
"x": 3,
|
88
|
+
"y": str(y),
|
89
|
+
"a": {"i": sub_i},
|
90
|
+
"z": 1,
|
91
|
+
}
|
92
|
+
)
|
93
|
+
|
94
|
+
self.store.register_book_channel(str(y), symbol)
|
95
|
+
y += 1
|
96
|
+
|
97
|
+
wsapp = self.client.ws_connect(
|
98
|
+
self.ws_url,
|
99
|
+
send_json=send_jsons,
|
100
|
+
hdlr_bytes=self.store.onmessage,
|
101
|
+
)
|
102
|
+
|
103
|
+
await wsapp._event.wait()
|
@@ -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)
|
@@ -0,0 +1,222 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
import json
|
5
|
+
import logging
|
6
|
+
from typing import Any, Awaitable, TYPE_CHECKING
|
7
|
+
|
8
|
+
from aiohttp import ClientResponse
|
9
|
+
from pybotters.store import DataStore, DataStoreCollection
|
10
|
+
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from pybotters.typedefs import Item
|
13
|
+
from pybotters.ws import ClientWebSocketResponse
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
def _accuracy_to_step(accuracy: int | str | None) -> str:
|
19
|
+
try:
|
20
|
+
n = int(accuracy) if accuracy is not None else 0
|
21
|
+
except (TypeError, ValueError): # pragma: no cover - defensive guard
|
22
|
+
n = 0
|
23
|
+
if n <= 0:
|
24
|
+
return "1"
|
25
|
+
return "0." + "0" * (n - 1) + "1"
|
26
|
+
|
27
|
+
|
28
|
+
class Book(DataStore):
|
29
|
+
"""LBank order book store parsed from the depth channel."""
|
30
|
+
|
31
|
+
_KEYS = ["id", "S", "p"]
|
32
|
+
|
33
|
+
def _init(self) -> None:
|
34
|
+
self.limit: int | None = None
|
35
|
+
self.symbol_map: dict[str, str] = {}
|
36
|
+
|
37
|
+
|
38
|
+
def _format_levels(
|
39
|
+
self,
|
40
|
+
levels: list[list[Any]],
|
41
|
+
channel_id: str | None,
|
42
|
+
side: str,
|
43
|
+
symbol: str | None,
|
44
|
+
) -> list[dict[str, Any]]:
|
45
|
+
formatted: list[dict[str, Any]] = []
|
46
|
+
for level in levels:
|
47
|
+
if len(level) < 2:
|
48
|
+
continue
|
49
|
+
try:
|
50
|
+
price = float(level[0])
|
51
|
+
size = float(level[1])
|
52
|
+
except (TypeError, ValueError): # pragma: no cover - defensive guard
|
53
|
+
continue
|
54
|
+
formatted.append(
|
55
|
+
{
|
56
|
+
"id": channel_id,
|
57
|
+
"S": side,
|
58
|
+
"p": price,
|
59
|
+
"q": size,
|
60
|
+
"s": symbol,
|
61
|
+
}
|
62
|
+
)
|
63
|
+
if self.limit is not None:
|
64
|
+
formatted = formatted[: self.limit]
|
65
|
+
return formatted
|
66
|
+
|
67
|
+
def _on_message(self, msg: Any) -> None:
|
68
|
+
|
69
|
+
data = json.loads(msg)
|
70
|
+
|
71
|
+
if not data:
|
72
|
+
return
|
73
|
+
|
74
|
+
channel_id = None
|
75
|
+
if data.get("y") is not None:
|
76
|
+
channel_id = str(data["y"])
|
77
|
+
|
78
|
+
symbol = None
|
79
|
+
if channel_id:
|
80
|
+
symbol = self.symbol_map.get(channel_id)
|
81
|
+
if symbol is None and data.get("i"):
|
82
|
+
symbol = self.symbol_map.get(str(data["i"]))
|
83
|
+
|
84
|
+
bids = self._format_levels(data.get("b", []), channel_id, "b", symbol)
|
85
|
+
asks = self._format_levels(data.get("s", []), channel_id, "a", symbol)
|
86
|
+
|
87
|
+
if not (bids or asks):
|
88
|
+
return
|
89
|
+
|
90
|
+
if channel_id is not None:
|
91
|
+
self._find_and_delete({"id": channel_id})
|
92
|
+
self._insert(bids + asks)
|
93
|
+
|
94
|
+
|
95
|
+
class Detail(DataStore):
|
96
|
+
"""Futures instrument metadata store obtained from the futures instrument endpoint."""
|
97
|
+
|
98
|
+
_KEYS = ["symbol"]
|
99
|
+
|
100
|
+
def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
|
101
|
+
try:
|
102
|
+
instrument:dict = entry["instrument"]
|
103
|
+
fee:dict = entry["fee"]
|
104
|
+
market_data:dict = entry["marketData"]
|
105
|
+
except (KeyError, TypeError):
|
106
|
+
return None
|
107
|
+
return {
|
108
|
+
"symbol": instrument.get("instrumentID"),
|
109
|
+
"instrument_name": instrument.get("instrumentName"),
|
110
|
+
"base_currency": instrument.get("baseCurrency"),
|
111
|
+
"price_currency": instrument.get("priceCurrency"),
|
112
|
+
"min_order_volume": instrument.get("minOrderVolume"),
|
113
|
+
"max_order_volume": instrument.get("maxOrderVolume"),
|
114
|
+
"tick_size": instrument.get("priceTick"),
|
115
|
+
"step_size": instrument.get("volumeTick"),
|
116
|
+
"maker_fee": fee.get("makerOpenFeeRate"),
|
117
|
+
"taker_fee": fee.get("takerOpenFeeRate"),
|
118
|
+
"last_price": market_data.get("lastPrice"),
|
119
|
+
"amount24": market_data.get("turnover24"),
|
120
|
+
}
|
121
|
+
|
122
|
+
def _onresponse(self, data: list[dict[str, Any]] | dict[str, Any] | None) -> None:
|
123
|
+
if not data:
|
124
|
+
self._clear()
|
125
|
+
return
|
126
|
+
entries = data
|
127
|
+
if isinstance(data, dict): # pragma: no cover - defensive guard
|
128
|
+
entries = data.get("data") or []
|
129
|
+
items: list[dict[str, Any]] = []
|
130
|
+
for entry in entries or []:
|
131
|
+
transformed = self._transform(entry)
|
132
|
+
if transformed:
|
133
|
+
items.append(transformed)
|
134
|
+
if not items:
|
135
|
+
self._clear()
|
136
|
+
return
|
137
|
+
self._clear()
|
138
|
+
self._insert(items)
|
139
|
+
|
140
|
+
|
141
|
+
class LbankDataStore(DataStoreCollection):
|
142
|
+
"""Aggregates book/detail stores for the LBank public feed."""
|
143
|
+
|
144
|
+
def _init(self) -> None:
|
145
|
+
self._create("book", datastore_class=Book)
|
146
|
+
self._create("detail", datastore_class=Detail)
|
147
|
+
self._channel_to_symbol: dict[str, str] = {}
|
148
|
+
|
149
|
+
@property
|
150
|
+
def book(self) -> Book:
|
151
|
+
"""
|
152
|
+
订单簿(Order Book)数据流,按订阅ID(channel_id)索引。
|
153
|
+
|
154
|
+
此属性表示通过深度频道(depth channel)接收到的订单簿快照和增量更新,数据结构示例如下:
|
155
|
+
|
156
|
+
Data structure:
|
157
|
+
[
|
158
|
+
{
|
159
|
+
"id": <channel_id>,
|
160
|
+
"S": "b" 或 "a", # "b" 表示买单,"a" 表示卖单
|
161
|
+
"p": <价格>,
|
162
|
+
"q": <数量>,
|
163
|
+
"s": <标准化交易对符号>
|
164
|
+
},
|
165
|
+
...
|
166
|
+
]
|
167
|
+
|
168
|
+
通过本属性可以获取当前 LBank 订单簿的最新状态,便于后续行情分析和撮合逻辑处理。
|
169
|
+
"""
|
170
|
+
return self._get("book")
|
171
|
+
|
172
|
+
@property
|
173
|
+
def detail(self) -> Detail:
|
174
|
+
"""
|
175
|
+
|
176
|
+
_KEYS = ["symbol"]
|
177
|
+
|
178
|
+
期货合约详情元数据流。
|
179
|
+
|
180
|
+
此属性表示通过期货合约接口获取的合约详情,包括合约ID、合约名称、基础币种、计价币种、最小/最大下单量、价格跳动、交易量跳动、maker/taker手续费率、最新价和24小时成交额等信息。
|
181
|
+
|
182
|
+
Data structure:
|
183
|
+
[
|
184
|
+
{
|
185
|
+
"symbol": "BTCUSDT", # 合约ID
|
186
|
+
"instrument_name": "BTCUSDT", # 合约名称
|
187
|
+
"base_currency": "BTC", # 基础币种
|
188
|
+
"price_currency": "USDT", # 计价币种
|
189
|
+
"min_order_volume": "0.0001", # 最小下单量
|
190
|
+
"max_order_volume": "600.0", # 最大下单量
|
191
|
+
"tick_size": "0.1", # 最小价格变动单位
|
192
|
+
"step_size": "0.0001", # 最小数量变动单位
|
193
|
+
"maker_fee": "0.0002", # Maker 手续费率
|
194
|
+
"taker_fee": "0.0006", # Taker 手续费率
|
195
|
+
"last_price": "117025.5", # 最新价
|
196
|
+
"amount24": "807363493.97579747" # 24小时成交额
|
197
|
+
},
|
198
|
+
...
|
199
|
+
]
|
200
|
+
|
201
|
+
通过本属性可以获取所有支持的期货合约元数据,便于下单参数校验和行情展示。
|
202
|
+
"""
|
203
|
+
return self._get("detail")
|
204
|
+
|
205
|
+
|
206
|
+
def register_book_channel(self, channel_id: str, symbol: str, *, raw_symbol: str | None = None) -> None:
|
207
|
+
if channel_id is not None:
|
208
|
+
self.book.symbol_map[str(channel_id)] = symbol
|
209
|
+
if raw_symbol:
|
210
|
+
self.book.symbol_map[str(raw_symbol)] = symbol
|
211
|
+
|
212
|
+
|
213
|
+
async def initialize(self, *aws: Awaitable[ClientResponse]) -> None:
|
214
|
+
for fut in asyncio.as_completed(aws):
|
215
|
+
res = await fut
|
216
|
+
data = await res.json()
|
217
|
+
if res.url.path == "/cfd/agg/v1/instrument":
|
218
|
+
self.detail._onresponse(data)
|
219
|
+
|
220
|
+
|
221
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
222
|
+
self.book._on_message(msg)
|
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.62
|
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,21 @@ 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
|
10
|
+
hyperquant/broker/lbank.py,sha256=e-DnZg-rBwpTV7FWmPpG89Y7kk7NZOzilvEn7V3LEI0,2978
|
9
11
|
hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
|
10
|
-
hyperquant/broker/ws.py,sha256=
|
12
|
+
hyperquant/broker/ws.py,sha256=Ce5-g2BWhS8ZmVoLjtSj3Eb1TNIN4pgEIyJ0ODjiwUo,1902
|
11
13
|
hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
|
12
14
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
15
|
+
hyperquant/broker/models/edgex.py,sha256=texNBwz_9r9CTl7dRNjvSm_toxV_og0TWnap3dqUk2s,15795
|
13
16
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
14
|
-
hyperquant/broker/models/
|
17
|
+
hyperquant/broker/models/lbank.py,sha256=_P0oRPID2Yd_cpElHa3QIT471YlfIzexXMrzm1WVnHI,7652
|
18
|
+
hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
|
15
19
|
hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
|
16
20
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
17
21
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
18
22
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
19
|
-
hyperquant-0.
|
20
|
-
hyperquant-0.
|
21
|
-
hyperquant-0.
|
23
|
+
hyperquant-0.62.dist-info/METADATA,sha256=cjL0r-jaGJWLIgCAXp-6RzRUdac8ec_bjEKJ2WtSCoo,4317
|
24
|
+
hyperquant-0.62.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
hyperquant-0.62.dist-info/RECORD,,
|
File without changes
|