hyperquant 0.61__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.
@@ -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,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)
|
@@ -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
|
@@ -7,17 +7,19 @@ hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
|
|
7
7
|
hyperquant/broker/auth.py,sha256=oA9Yw1I59-u0Tnoj2e4wUup5q8V5T2qpga5RKbiAiZI,2614
|
8
8
|
hyperquant/broker/edgex.py,sha256=qQtc8jZqB5ZODoGGVcG_aIVUlrJX_pRF9EyO927LiVM,6646
|
9
9
|
hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
|
10
|
+
hyperquant/broker/lbank.py,sha256=e-DnZg-rBwpTV7FWmPpG89Y7kk7NZOzilvEn7V3LEI0,2978
|
10
11
|
hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
|
11
12
|
hyperquant/broker/ws.py,sha256=Ce5-g2BWhS8ZmVoLjtSj3Eb1TNIN4pgEIyJ0ODjiwUo,1902
|
12
13
|
hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
|
13
14
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
14
15
|
hyperquant/broker/models/edgex.py,sha256=texNBwz_9r9CTl7dRNjvSm_toxV_og0TWnap3dqUk2s,15795
|
15
16
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
17
|
+
hyperquant/broker/models/lbank.py,sha256=_P0oRPID2Yd_cpElHa3QIT471YlfIzexXMrzm1WVnHI,7652
|
16
18
|
hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
|
17
19
|
hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
|
18
20
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
19
21
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
20
22
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
21
|
-
hyperquant-0.
|
22
|
-
hyperquant-0.
|
23
|
-
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
|