hyperquant 0.6__py3-none-any.whl → 0.8__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/auth.py +144 -9
- hyperquant/broker/bitget.py +311 -0
- hyperquant/broker/edgex.py +331 -14
- hyperquant/broker/lbank.py +588 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/models/bitget.py +359 -0
- hyperquant/broker/models/edgex.py +545 -5
- hyperquant/broker/models/lbank.py +557 -0
- hyperquant/broker/ws.py +21 -3
- {hyperquant-0.6.dist-info → hyperquant-0.8.dist-info}/METADATA +1 -1
- {hyperquant-0.6.dist-info → hyperquant-0.8.dist-info}/RECORD +13 -7
- {hyperquant-0.6.dist-info → hyperquant-0.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,557 @@
|
|
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
|
+
|
39
|
+
|
40
|
+
def _on_message(self, msg: Any) -> None:
|
41
|
+
|
42
|
+
data = json.loads(msg)
|
43
|
+
|
44
|
+
if not data:
|
45
|
+
return
|
46
|
+
|
47
|
+
channel_id = None
|
48
|
+
if data.get("y") is not None:
|
49
|
+
channel_id = str(data["y"])
|
50
|
+
|
51
|
+
symbol = None
|
52
|
+
if channel_id:
|
53
|
+
symbol = self.symbol_map.get(channel_id)
|
54
|
+
if symbol is None and data.get("i"):
|
55
|
+
symbol = self.symbol_map.get(str(data["i"]))
|
56
|
+
|
57
|
+
bids = data.get("b", [])
|
58
|
+
asks = data.get("s", [])
|
59
|
+
if not (bids or asks):
|
60
|
+
return
|
61
|
+
bids = bids[: self.limit] if self.limit else bids
|
62
|
+
asks = asks[: self.limit] if self.limit else asks
|
63
|
+
bids = [
|
64
|
+
{"id": channel_id, "S": "b", "p": str(item[0]), "q": str(item[1]), "s": symbol}
|
65
|
+
for item in bids
|
66
|
+
]
|
67
|
+
asks = [
|
68
|
+
{"id": channel_id, "S": "a", "p": str(item[0]), "q": str(item[1]), "s": symbol}
|
69
|
+
for item in asks
|
70
|
+
]
|
71
|
+
|
72
|
+
|
73
|
+
if channel_id is not None:
|
74
|
+
self._find_and_delete({"id": channel_id})
|
75
|
+
self._insert(bids + asks)
|
76
|
+
|
77
|
+
|
78
|
+
class Detail(DataStore):
|
79
|
+
"""Futures instrument metadata store obtained from the futures instrument endpoint."""
|
80
|
+
|
81
|
+
_KEYS = ["symbol"]
|
82
|
+
|
83
|
+
def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
|
84
|
+
try:
|
85
|
+
instrument:dict = entry["instrument"]
|
86
|
+
fee:dict = entry["fee"]
|
87
|
+
market_data:dict = entry["marketData"]
|
88
|
+
except (KeyError, TypeError):
|
89
|
+
return None
|
90
|
+
return {
|
91
|
+
"symbol": instrument.get("instrumentID"),
|
92
|
+
"instrument_name": instrument.get("instrumentName"),
|
93
|
+
"base_currency": instrument.get("baseCurrency"),
|
94
|
+
"price_currency": instrument.get("priceCurrency"),
|
95
|
+
"min_order_volume": instrument.get("minOrderVolume"),
|
96
|
+
"max_order_volume": instrument.get("maxOrderVolume"),
|
97
|
+
"tick_size": instrument.get("priceTick"),
|
98
|
+
"step_size": instrument.get("volumeTick"),
|
99
|
+
"maker_fee": fee.get("makerOpenFeeRate"),
|
100
|
+
"taker_fee": fee.get("takerOpenFeeRate"),
|
101
|
+
"last_price": market_data.get("lastPrice"),
|
102
|
+
"amount24": market_data.get("turnover24"),
|
103
|
+
}
|
104
|
+
|
105
|
+
def _onresponse(self, data: list[dict[str, Any]] | dict[str, Any] | None) -> None:
|
106
|
+
if not data:
|
107
|
+
self._clear()
|
108
|
+
return
|
109
|
+
entries = data
|
110
|
+
if isinstance(data, dict): # pragma: no cover - defensive guard
|
111
|
+
entries = data.get("data") or []
|
112
|
+
items: list[dict[str, Any]] = []
|
113
|
+
for entry in entries or []:
|
114
|
+
transformed = self._transform(entry)
|
115
|
+
if transformed:
|
116
|
+
items.append(transformed)
|
117
|
+
if not items:
|
118
|
+
self._clear()
|
119
|
+
return
|
120
|
+
self._clear()
|
121
|
+
self._insert(items)
|
122
|
+
|
123
|
+
|
124
|
+
class Orders(DataStore):
|
125
|
+
"""Active order snapshots fetched via the REST order query."""
|
126
|
+
|
127
|
+
_KEYS = ["order_id"]
|
128
|
+
|
129
|
+
_ORDER_STATUS_MAP = {
|
130
|
+
"1": "filled",
|
131
|
+
"2": "filled",
|
132
|
+
"4": "open",
|
133
|
+
"5": "partially_filled",
|
134
|
+
"6": "canceled",
|
135
|
+
}
|
136
|
+
|
137
|
+
_DIRECTION_MAP = {
|
138
|
+
"0": "buy",
|
139
|
+
"1": "sell",
|
140
|
+
}
|
141
|
+
|
142
|
+
_OFFSET_FLAG_MAP = {
|
143
|
+
"0": "open",
|
144
|
+
"1": "close",
|
145
|
+
}
|
146
|
+
|
147
|
+
def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
|
148
|
+
if not entry:
|
149
|
+
return None
|
150
|
+
|
151
|
+
order_id = entry.get("OrderSysID") or entry.get("orderSysID")
|
152
|
+
if not order_id:
|
153
|
+
return None
|
154
|
+
|
155
|
+
direction = self._DIRECTION_MAP.get(str(entry.get("Direction")), str(entry.get("Direction")))
|
156
|
+
offset_flag = self._OFFSET_FLAG_MAP.get(
|
157
|
+
str(entry.get("OffsetFlag")), str(entry.get("OffsetFlag"))
|
158
|
+
)
|
159
|
+
|
160
|
+
order_price_type = str(entry.get("OrderPriceType")) if entry.get("OrderPriceType") is not None else None
|
161
|
+
order_type = str(entry.get("OrderType")) if entry.get("OrderType") is not None else None
|
162
|
+
|
163
|
+
if order_price_type == "4":
|
164
|
+
order_kind = "market"
|
165
|
+
elif order_type == "1":
|
166
|
+
order_kind = "limit_fak"
|
167
|
+
else:
|
168
|
+
order_kind = "limit"
|
169
|
+
|
170
|
+
status_code = str(entry.get("OrderStatus")) if entry.get("OrderStatus") is not None else None
|
171
|
+
status = self._ORDER_STATUS_MAP.get(status_code, status_code)
|
172
|
+
|
173
|
+
client_order_id = (
|
174
|
+
entry.get("LocalID")
|
175
|
+
or entry.get("localID")
|
176
|
+
or entry.get("LocalId")
|
177
|
+
or entry.get("localId")
|
178
|
+
)
|
179
|
+
|
180
|
+
return {
|
181
|
+
"order_id": order_id,
|
182
|
+
"client_order_id": client_order_id,
|
183
|
+
"symbol": entry.get("InstrumentID"),
|
184
|
+
"side": direction,
|
185
|
+
"offset": offset_flag,
|
186
|
+
"order_type": order_kind,
|
187
|
+
"price": entry.get('TradePrice') or entry.get("Price"),
|
188
|
+
"order_price": entry.get("OrderPrice") or entry.get('Price'),
|
189
|
+
"quantity": entry.get("Volume"),
|
190
|
+
"filled": entry.get("VolumeTraded"),
|
191
|
+
"remaining": entry.get("VolumeRemain"),
|
192
|
+
"status": status,
|
193
|
+
"status_code": status_code,
|
194
|
+
"position_id": entry.get("PositionID"),
|
195
|
+
"leverage": entry.get("Leverage"),
|
196
|
+
"frozen_margin": entry.get("FrozenMargin"),
|
197
|
+
"frozen_fee": entry.get("FrozenFee"),
|
198
|
+
"insert_time": entry.get("InsertTime"),
|
199
|
+
"update_time": entry.get("UpdateTime"),
|
200
|
+
}
|
201
|
+
|
202
|
+
@staticmethod
|
203
|
+
def _extract_rows(data: dict[str, Any] | None) -> list[dict[str, Any]]:
|
204
|
+
if not data:
|
205
|
+
return []
|
206
|
+
payload = data.get("data") if isinstance(data, dict) else None
|
207
|
+
if isinstance(payload, dict):
|
208
|
+
rows = payload.get("data")
|
209
|
+
if isinstance(rows, list):
|
210
|
+
return rows
|
211
|
+
if isinstance(payload, list): # pragma: no cover - defensive path
|
212
|
+
return payload
|
213
|
+
return []
|
214
|
+
|
215
|
+
def _onresponse(self, data: dict[str, Any] | None) -> None:
|
216
|
+
rows = self._extract_rows(data)
|
217
|
+
if not rows:
|
218
|
+
self._clear()
|
219
|
+
return
|
220
|
+
|
221
|
+
items: list[dict[str, Any]] = []
|
222
|
+
for row in rows:
|
223
|
+
transformed = self._transform(row)
|
224
|
+
if transformed:
|
225
|
+
items.append(transformed)
|
226
|
+
|
227
|
+
self._clear()
|
228
|
+
if items:
|
229
|
+
self._insert(items)
|
230
|
+
|
231
|
+
|
232
|
+
class OrderFinish(Orders):
|
233
|
+
"""Finished order snapshots fetched from the historical REST endpoint."""
|
234
|
+
|
235
|
+
def _onresponse(self, data: dict[str, Any] | None) -> None:
|
236
|
+
rows: list[dict[str, Any]] = []
|
237
|
+
if isinstance(data, dict):
|
238
|
+
payload = data.get("data") or {}
|
239
|
+
if isinstance(payload, dict):
|
240
|
+
list_payload = payload.get("list") or {}
|
241
|
+
if isinstance(list_payload, dict):
|
242
|
+
rows = list_payload.get("resultList") or []
|
243
|
+
|
244
|
+
if not rows:
|
245
|
+
self._clear()
|
246
|
+
return
|
247
|
+
|
248
|
+
items: list[dict[str, Any]] = []
|
249
|
+
for row in rows:
|
250
|
+
transformed = self._transform(row)
|
251
|
+
if transformed:
|
252
|
+
items.append(transformed)
|
253
|
+
|
254
|
+
self._clear()
|
255
|
+
if items:
|
256
|
+
self._insert(items)
|
257
|
+
|
258
|
+
|
259
|
+
class Position(DataStore):
|
260
|
+
"""Open position snapshots fetched from the REST position endpoint."""
|
261
|
+
|
262
|
+
_KEYS = ["position_id"]
|
263
|
+
|
264
|
+
_POS_DIRECTION_MAP = {
|
265
|
+
"1": "net",
|
266
|
+
"2": "long",
|
267
|
+
"3": "short",
|
268
|
+
}
|
269
|
+
|
270
|
+
def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
|
271
|
+
if not entry:
|
272
|
+
return None
|
273
|
+
position_id = entry.get("PositionID")
|
274
|
+
bus_id = entry.get("BusinessNo")
|
275
|
+
if not position_id:
|
276
|
+
return None
|
277
|
+
|
278
|
+
q = float(entry.get("Position", 0))
|
279
|
+
side = "net"
|
280
|
+
if q > 0:
|
281
|
+
side = "long"
|
282
|
+
elif q < 0:
|
283
|
+
side = "short"
|
284
|
+
|
285
|
+
return {
|
286
|
+
"position_id": position_id,
|
287
|
+
"bus_id": bus_id,
|
288
|
+
"symbol": entry.get("InstrumentID"),
|
289
|
+
"side": side,
|
290
|
+
"quantity": entry.get("Position"),
|
291
|
+
"available": entry.get("AvailableUse"),
|
292
|
+
"avg_price": entry.get("OpenPrice"),
|
293
|
+
"entry_price": entry.get("OpenPrice"),
|
294
|
+
"leverage": entry.get("Leverage"),
|
295
|
+
"liquidation_price": entry.get("estimateLiquidationPrice") or entry.get("FORCECLOSEPRICE"),
|
296
|
+
"margin_used": entry.get("UseMargin"),
|
297
|
+
"unrealized_pnl": entry.get("PositionFee"),
|
298
|
+
"realized_pnl": entry.get("CloseProfit"),
|
299
|
+
"update_time": entry.get("UpdateTime"),
|
300
|
+
"insert_time": entry.get("InsertTime"),
|
301
|
+
"begin_time": entry.get("BeginTime")
|
302
|
+
}
|
303
|
+
|
304
|
+
def _onresponse(self, data: dict[str, Any] | None) -> None:
|
305
|
+
rows = Orders._extract_rows(data) # reuse helper for nested payload
|
306
|
+
if not rows:
|
307
|
+
self._clear()
|
308
|
+
return
|
309
|
+
|
310
|
+
items: list[dict[str, Any]] = []
|
311
|
+
for row in rows:
|
312
|
+
transformed = self._transform(row)
|
313
|
+
if transformed:
|
314
|
+
items.append(transformed)
|
315
|
+
|
316
|
+
self._clear()
|
317
|
+
if items:
|
318
|
+
self._insert(items)
|
319
|
+
|
320
|
+
|
321
|
+
class Balance(DataStore):
|
322
|
+
"""Account balance snapshot derived from sendQryAll endpoint."""
|
323
|
+
|
324
|
+
_KEYS = ["asset"]
|
325
|
+
|
326
|
+
def _init(self) -> None:
|
327
|
+
self._asset: str | None = None
|
328
|
+
|
329
|
+
def set_asset(self, asset: str | None) -> None:
|
330
|
+
self._asset = asset
|
331
|
+
|
332
|
+
def _transform(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
333
|
+
if not payload:
|
334
|
+
return None
|
335
|
+
asset_balance = payload.get("assetBalance") or {}
|
336
|
+
if not asset_balance:
|
337
|
+
return None
|
338
|
+
|
339
|
+
asset = payload.get("asset") or asset_balance.get("currency") or self._asset or "USDT"
|
340
|
+
|
341
|
+
return {
|
342
|
+
"asset": asset,
|
343
|
+
"balance": asset_balance.get("balance"),
|
344
|
+
"available": asset_balance.get("available"),
|
345
|
+
"real_available": asset_balance.get("realAvailable"),
|
346
|
+
"frozen_margin": asset_balance.get("frozenMargin"),
|
347
|
+
"frozen_fee": asset_balance.get("frozenFee"),
|
348
|
+
"total_close_profit": asset_balance.get("totalCloseProfit"),
|
349
|
+
"cross_margin": asset_balance.get("crossMargin"),
|
350
|
+
}
|
351
|
+
|
352
|
+
def _onresponse(self, data: dict[str, Any] | None) -> None:
|
353
|
+
payload: dict[str, Any] = {}
|
354
|
+
if isinstance(data, dict):
|
355
|
+
payload = data.get("data") or {}
|
356
|
+
|
357
|
+
item = self._transform(payload)
|
358
|
+
self._clear()
|
359
|
+
if item:
|
360
|
+
self._insert([item])
|
361
|
+
|
362
|
+
|
363
|
+
class LbankDataStore(DataStoreCollection):
|
364
|
+
"""Aggregates book/detail stores for the LBank public feed."""
|
365
|
+
|
366
|
+
def _init(self) -> None:
|
367
|
+
self._create("book", datastore_class=Book)
|
368
|
+
self._create("detail", datastore_class=Detail)
|
369
|
+
self._create("orders", datastore_class=Orders)
|
370
|
+
self._create("order_finish", datastore_class=OrderFinish)
|
371
|
+
self._create("position", datastore_class=Position)
|
372
|
+
self._create("balance", datastore_class=Balance)
|
373
|
+
self._channel_to_symbol: dict[str, str] = {}
|
374
|
+
|
375
|
+
@property
|
376
|
+
def book(self) -> Book:
|
377
|
+
"""
|
378
|
+
订单簿(Order Book)数据流,按订阅ID(channel_id)索引。
|
379
|
+
|
380
|
+
此属性表示通过深度频道(depth channel)接收到的订单簿快照和增量更新,数据结构示例如下:
|
381
|
+
|
382
|
+
Data structure:
|
383
|
+
[
|
384
|
+
{
|
385
|
+
"id": <channel_id>,
|
386
|
+
"S": "b" 或 "a", # "b" 表示买单,"a" 表示卖单
|
387
|
+
"p": <价格>,
|
388
|
+
"q": <数量>,
|
389
|
+
"s": <标准化交易对符号>
|
390
|
+
},
|
391
|
+
...
|
392
|
+
]
|
393
|
+
|
394
|
+
通过本属性可以获取当前 LBank 订单簿的最新状态,便于后续行情分析和撮合逻辑处理。
|
395
|
+
"""
|
396
|
+
return self._get("book")
|
397
|
+
|
398
|
+
@property
|
399
|
+
def detail(self) -> Detail:
|
400
|
+
"""
|
401
|
+
|
402
|
+
_KEYS = ["symbol"]
|
403
|
+
|
404
|
+
期货合约详情元数据流。
|
405
|
+
|
406
|
+
此属性表示通过期货合约接口获取的合约详情,包括合约ID、合约名称、基础币种、计价币种、最小/最大下单量、价格跳动、交易量跳动、maker/taker手续费率、最新价和24小时成交额等信息。
|
407
|
+
|
408
|
+
Data structure:
|
409
|
+
[
|
410
|
+
{
|
411
|
+
"symbol": "BTCUSDT", # 合约ID
|
412
|
+
"instrument_name": "BTCUSDT", # 合约名称
|
413
|
+
"base_currency": "BTC", # 基础币种
|
414
|
+
"price_currency": "USDT", # 计价币种
|
415
|
+
"min_order_volume": "0.0001", # 最小下单量
|
416
|
+
"max_order_volume": "600.0", # 最大下单量
|
417
|
+
"tick_size": "0.1", # 最小价格变动单位
|
418
|
+
"step_size": "0.0001", # 最小数量变动单位
|
419
|
+
"maker_fee": "0.0002", # Maker 手续费率
|
420
|
+
"taker_fee": "0.0006", # Taker 手续费率
|
421
|
+
"last_price": "117025.5", # 最新价
|
422
|
+
"amount24": "807363493.97579747" # 24小时成交额
|
423
|
+
},
|
424
|
+
...
|
425
|
+
]
|
426
|
+
|
427
|
+
通过本属性可以获取所有支持的期货合约元数据,便于下单参数校验和行情展示。
|
428
|
+
"""
|
429
|
+
return self._get("detail")
|
430
|
+
|
431
|
+
@property
|
432
|
+
def orders(self) -> Orders:
|
433
|
+
"""
|
434
|
+
活跃订单数据流。
|
435
|
+
|
436
|
+
此属性表示通过 REST 接口获取的当前活跃订单快照,包括已开仓订单、部分成交订单等状态。
|
437
|
+
|
438
|
+
Data structure:
|
439
|
+
[
|
440
|
+
{
|
441
|
+
"order_id": <系统订单ID>,
|
442
|
+
"client_order_id": <用户自定义订单ID>,
|
443
|
+
"symbol": <合约ID>,
|
444
|
+
"side": "buy" 或 "sell",
|
445
|
+
"offset": "open" 或 "close",
|
446
|
+
"order_type": "limit" / "market" / "limit_fak",
|
447
|
+
"price": <下单价格>,
|
448
|
+
"quantity": <下单数量>,
|
449
|
+
"filled": <已成交数量>,
|
450
|
+
"remaining": <剩余数量>,
|
451
|
+
"status": <订单状态>,
|
452
|
+
"status_code": <原始状态码>,
|
453
|
+
"position_id": <关联仓位ID>,
|
454
|
+
"leverage": <杠杆倍数>,
|
455
|
+
"frozen_margin": <冻结保证金>,
|
456
|
+
"frozen_fee": <冻结手续费>,
|
457
|
+
"insert_time": <下单时间>,
|
458
|
+
"update_time": <更新时间>
|
459
|
+
},
|
460
|
+
...
|
461
|
+
]
|
462
|
+
|
463
|
+
通过本属性可以跟踪当前活跃订单状态,便于订单管理和风控。
|
464
|
+
"""
|
465
|
+
return self._get("orders")
|
466
|
+
|
467
|
+
@property
|
468
|
+
def order_finish(self) -> OrderFinish:
|
469
|
+
"""历史已完成订单数据流,与 ``orders`` 字段保持兼容。"""
|
470
|
+
return self._get("order_finish")
|
471
|
+
|
472
|
+
@property
|
473
|
+
def position(self) -> Position:
|
474
|
+
"""
|
475
|
+
持仓数据流。
|
476
|
+
|
477
|
+
此属性表示通过 REST 接口获取的当前持仓快照,包括多头、空头或净持仓等方向信息。
|
478
|
+
|
479
|
+
Data structure:
|
480
|
+
[
|
481
|
+
{
|
482
|
+
"position_id": <仓位ID>,
|
483
|
+
"bus_id": <订单ID覆盖>,
|
484
|
+
"symbol": <合约ID>,
|
485
|
+
"side": "long" / "short" / "net",
|
486
|
+
"quantity": <持仓数量>,
|
487
|
+
"available": <可用数量>,
|
488
|
+
"avg_price": <持仓均价>,
|
489
|
+
"entry_price": <开仓均价>,
|
490
|
+
"leverage": <杠杆倍数>,
|
491
|
+
"liquidation_price": <预估强平价>,
|
492
|
+
"margin_used": <已用保证金>,
|
493
|
+
"unrealized_pnl": <未实现盈亏>,
|
494
|
+
"realized_pnl": <已实现盈亏>,
|
495
|
+
"update_time": <更新时间>,
|
496
|
+
"insert_time": <插入时间>,
|
497
|
+
"begin_time": <持仓开始时间>
|
498
|
+
},
|
499
|
+
...
|
500
|
+
]
|
501
|
+
|
502
|
+
通过本属性可以跟踪账户当前仓位状态,便于盈亏分析和风控。
|
503
|
+
"""
|
504
|
+
return self._get("position")
|
505
|
+
|
506
|
+
@property
|
507
|
+
def balance(self) -> Balance:
|
508
|
+
"""
|
509
|
+
账户余额数据流。
|
510
|
+
|
511
|
+
此属性表示通过 REST 接口获取的账户资产快照,包括余额、可用余额、保证金等信息。
|
512
|
+
|
513
|
+
Data structure:
|
514
|
+
[
|
515
|
+
{
|
516
|
+
"asset": <资产币种>,
|
517
|
+
"balance": <总余额>,
|
518
|
+
"available": <可用余额>,
|
519
|
+
"real_available": <实际可用余额>,
|
520
|
+
"frozen_margin": <冻结保证金>,
|
521
|
+
"frozen_fee": <冻结手续费>,
|
522
|
+
"total_close_profit": <累计平仓收益>,
|
523
|
+
"cross_margin": <全仓保证金>
|
524
|
+
}
|
525
|
+
]
|
526
|
+
|
527
|
+
通过本属性可以跟踪账户余额与资金情况,便于资金管理和风险控制。
|
528
|
+
"""
|
529
|
+
return self._get("balance")
|
530
|
+
|
531
|
+
|
532
|
+
def register_book_channel(self, channel_id: str, symbol: str, *, raw_symbol: str | None = None) -> None:
|
533
|
+
if channel_id is not None:
|
534
|
+
self.book.symbol_map[str(channel_id)] = symbol
|
535
|
+
if raw_symbol:
|
536
|
+
self.book.symbol_map[str(raw_symbol)] = symbol
|
537
|
+
|
538
|
+
|
539
|
+
async def initialize(self, *aws: Awaitable[ClientResponse]) -> None:
|
540
|
+
for fut in asyncio.as_completed(aws):
|
541
|
+
res = await fut
|
542
|
+
data = await res.json()
|
543
|
+
|
544
|
+
if res.url.path == "/cfd/agg/v1/instrument":
|
545
|
+
self.detail._onresponse(data)
|
546
|
+
if res.url.path == "/cfd/query/v1.0/Order":
|
547
|
+
self.orders._onresponse(data)
|
548
|
+
if res.url.path == "/cfd/query/v1.0/Position":
|
549
|
+
self.position._onresponse(data)
|
550
|
+
if res.url.path == "/cfd/agg/v1/sendQryAll":
|
551
|
+
self.balance._onresponse(data)
|
552
|
+
if res.url.path == "/cfd/cff/v1/FinishOrder":
|
553
|
+
self.order_finish._onresponse(data)
|
554
|
+
|
555
|
+
|
556
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
557
|
+
self.book._on_message(msg)
|
hyperquant/broker/ws.py
CHANGED
@@ -1,7 +1,12 @@
|
|
1
1
|
import asyncio
|
2
|
+
import base64
|
3
|
+
import time
|
4
|
+
from typing import Any
|
5
|
+
|
2
6
|
import pybotters
|
3
7
|
from pybotters.ws import ClientWebSocketResponse, logger
|
4
8
|
from pybotters.auth import Hosts
|
9
|
+
import urllib
|
5
10
|
import yarl
|
6
11
|
|
7
12
|
|
@@ -17,8 +22,23 @@ class Heartbeat:
|
|
17
22
|
await ws.send_str('{"method":"ping"}')
|
18
23
|
await asyncio.sleep(10.0)
|
19
24
|
|
25
|
+
@staticmethod
|
26
|
+
async def edgex(ws: pybotters.ws.ClientWebSocketResponse):
|
27
|
+
while not ws.closed:
|
28
|
+
now = str(int(time.time() * 1000))
|
29
|
+
await ws.send_json({"type": "ping", "time": now})
|
30
|
+
await asyncio.sleep(20.0)
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
async def lbank(ws: ClientWebSocketResponse):
|
34
|
+
while not ws.closed:
|
35
|
+
await ws.send_str('ping')
|
36
|
+
await asyncio.sleep(6)
|
37
|
+
|
20
38
|
pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
|
21
39
|
pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
|
40
|
+
pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
|
41
|
+
pybotters.ws.HeartbeatHosts.items['uuws.rerrkvifj.com'] = Heartbeat.lbank
|
22
42
|
|
23
43
|
class WssAuth:
|
24
44
|
@staticmethod
|
@@ -42,7 +62,5 @@ class WssAuth:
|
|
42
62
|
break
|
43
63
|
else:
|
44
64
|
logger.warning(f"WebSocket login failed: {data}")
|
45
|
-
|
46
|
-
|
47
|
-
|
65
|
+
|
48
66
|
pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyperquant
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8
|
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
|
@@ -4,20 +4,26 @@ hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
|
|
4
4
|
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
|
-
hyperquant/broker/auth.py,sha256=
|
8
|
-
hyperquant/broker/
|
7
|
+
hyperquant/broker/auth.py,sha256=Wst7mTBuUS2BQ5hZd0a8FNNs5Uc01ac9WzJpseTuyAY,7673
|
8
|
+
hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
|
9
|
+
hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
|
9
10
|
hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
|
11
|
+
hyperquant/broker/lbank.py,sha256=ynlBA7TAPjsT_aaAh_G4jse8gNLYdmuQQcDaWSjcxL8,19559
|
10
12
|
hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
|
11
|
-
hyperquant/broker/ws.py,sha256=
|
13
|
+
hyperquant/broker/ws.py,sha256=9Zu5JSLj-ylYEVmFmRwvZDDnVYKwb37cLHfZzA0AZGc,2200
|
14
|
+
hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
|
12
15
|
hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
|
13
16
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
14
|
-
hyperquant/broker/
|
17
|
+
hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
|
18
|
+
hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
|
19
|
+
hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
|
15
20
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
21
|
+
hyperquant/broker/models/lbank.py,sha256=vHkNKxIMzpoC_EwcZnEOPOupizF92yGWi9GKxvYYFUQ,19181
|
16
22
|
hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
|
17
23
|
hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
|
18
24
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
19
25
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
20
26
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
21
|
-
hyperquant-0.
|
22
|
-
hyperquant-0.
|
23
|
-
hyperquant-0.
|
27
|
+
hyperquant-0.8.dist-info/METADATA,sha256=sZJCGMDc0ZTKhOfpK8IK7H0XkgKnhQJMX-Fai7aqahM,4316
|
28
|
+
hyperquant-0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
29
|
+
hyperquant-0.8.dist-info/RECORD,,
|
File without changes
|