hyperquant 0.7__py3-none-any.whl → 0.9__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.
Potentially problematic release.
This version of hyperquant might be problematic. Click here for more details.
- hyperquant/broker/auth.py +74 -1
- hyperquant/broker/bitget.py +245 -35
- hyperquant/broker/coinup.py +556 -0
- hyperquant/broker/coinw.py +487 -0
- hyperquant/broker/lbank.py +312 -5
- hyperquant/broker/models/bitget.py +84 -8
- hyperquant/broker/models/coinup.py +290 -0
- hyperquant/broker/models/coinw.py +724 -0
- hyperquant/broker/models/lbank.py +16 -6
- hyperquant/broker/ws.py +53 -4
- {hyperquant-0.7.dist-info → hyperquant-0.9.dist-info}/METADATA +4 -2
- {hyperquant-0.7.dist-info → hyperquant-0.9.dist-info}/RECORD +13 -9
- {hyperquant-0.7.dist-info → hyperquant-0.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,724 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Awaitable
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
from pybotters.store import DataStore, DataStoreCollection
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from pybotters.typedefs import Item
|
|
12
|
+
from pybotters.ws import ClientWebSocketResponse
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Book(DataStore):
|
|
16
|
+
"""CoinW 合约订单簿数据存储。
|
|
17
|
+
|
|
18
|
+
WebSocket 频道: futures/depth
|
|
19
|
+
|
|
20
|
+
消息示例(来源: https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
|
|
21
|
+
|
|
22
|
+
.. code:: json
|
|
23
|
+
|
|
24
|
+
{
|
|
25
|
+
"biz": "futures",
|
|
26
|
+
"pairCode": "BTC",
|
|
27
|
+
"type": "depth",
|
|
28
|
+
"data": {
|
|
29
|
+
"asks": [{"p": "95640.3", "m": "0.807"}, ...],
|
|
30
|
+
"bids": [{"p": "95640.2", "m": "0.068"}, ...]
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
_KEYS = ["s", "S", "p", "q"]
|
|
36
|
+
|
|
37
|
+
def _init(self) -> None:
|
|
38
|
+
self.limit: int | None = None
|
|
39
|
+
self._last_update: float = 0.0
|
|
40
|
+
|
|
41
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
42
|
+
data = msg.get("data")
|
|
43
|
+
if not isinstance(data, dict):
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
asks = data.get("asks") or []
|
|
47
|
+
bids = data.get("bids") or []
|
|
48
|
+
if not asks and not bids:
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
symbol = (
|
|
52
|
+
msg.get("pairCode")
|
|
53
|
+
or data.get("pairCode")
|
|
54
|
+
or msg.get("symbol")
|
|
55
|
+
or data.get("symbol")
|
|
56
|
+
)
|
|
57
|
+
if not symbol:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if self.limit is not None:
|
|
61
|
+
asks = asks[: self.limit]
|
|
62
|
+
bids = bids[: self.limit]
|
|
63
|
+
|
|
64
|
+
entries: list[dict[str, Any]] = []
|
|
65
|
+
for side, levels in (("a", asks), ("b", bids)):
|
|
66
|
+
for level in levels:
|
|
67
|
+
price = level.get("p") or level.get("price")
|
|
68
|
+
size = level.get("m") or level.get("q") or level.get("size")
|
|
69
|
+
if price is None or size is None:
|
|
70
|
+
continue
|
|
71
|
+
entries.append(
|
|
72
|
+
{
|
|
73
|
+
"s": str(symbol),
|
|
74
|
+
"S": side,
|
|
75
|
+
"p": str(price),
|
|
76
|
+
"q": str(size),
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not entries:
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
self._find_and_delete({"s": str(symbol)})
|
|
84
|
+
self._insert(entries)
|
|
85
|
+
self._last_update = time.time()
|
|
86
|
+
|
|
87
|
+
def sorted(
|
|
88
|
+
self, query: Item | None = None, limit: int | None = None
|
|
89
|
+
) -> dict[str, list[Item]]:
|
|
90
|
+
return self._sorted(
|
|
91
|
+
item_key="S",
|
|
92
|
+
item_asc_key="a",
|
|
93
|
+
item_desc_key="b",
|
|
94
|
+
sort_key="p",
|
|
95
|
+
query=query,
|
|
96
|
+
limit=limit,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def last_update(self) -> float:
|
|
101
|
+
return self._last_update
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class Detail(DataStore):
|
|
105
|
+
"""CoinW 合约信息数据存储。
|
|
106
|
+
|
|
107
|
+
文档: https://www.coinw.com/api-doc/futures-trading/market/get-instrument-information
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
_KEYS = ["name"]
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def _transform(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
114
|
+
if not entry:
|
|
115
|
+
return None
|
|
116
|
+
transformed = dict(entry)
|
|
117
|
+
base = entry.get("name") or entry.get("base")
|
|
118
|
+
quote = entry.get("quote")
|
|
119
|
+
pricePrecision = entry.get("pricePrecision")
|
|
120
|
+
transformed['tick_size'] = 10 ** (-int(pricePrecision))
|
|
121
|
+
transformed['step_size'] = entry.get("oneLotSize")
|
|
122
|
+
if base and quote:
|
|
123
|
+
transformed.setdefault(
|
|
124
|
+
"symbol", f"{str(base).upper()}_{str(quote).upper()}"
|
|
125
|
+
)
|
|
126
|
+
return transformed
|
|
127
|
+
|
|
128
|
+
def _onresponse(self, data: Any) -> None:
|
|
129
|
+
if data is None:
|
|
130
|
+
self._clear()
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
entries: list[dict[str, Any]]
|
|
134
|
+
if isinstance(data, dict):
|
|
135
|
+
entries = data.get("data") or []
|
|
136
|
+
else:
|
|
137
|
+
entries = data
|
|
138
|
+
|
|
139
|
+
items: list[dict[str, Any]] = []
|
|
140
|
+
for entry in entries:
|
|
141
|
+
if not isinstance(entry, dict):
|
|
142
|
+
continue
|
|
143
|
+
transformed = self._transform(entry)
|
|
144
|
+
if transformed:
|
|
145
|
+
items.append(transformed)
|
|
146
|
+
|
|
147
|
+
self._clear()
|
|
148
|
+
if items:
|
|
149
|
+
self._insert(items)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class Ticker(DataStore):
|
|
153
|
+
"""CoinW 24h 交易摘要数据存储。
|
|
154
|
+
|
|
155
|
+
文档: https://www.coinw.com/api-doc/futures-trading/market/get-last-trade-summary-of-all-instruments
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
_KEYS = ["name"]
|
|
159
|
+
|
|
160
|
+
@staticmethod
|
|
161
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
162
|
+
instrument = entry.get("instrument") or entry.get("symbol") or entry.get("pairCode")
|
|
163
|
+
if not instrument:
|
|
164
|
+
return None
|
|
165
|
+
normalized = dict(entry)
|
|
166
|
+
normalized["instrument"] = str(instrument).upper()
|
|
167
|
+
return normalized
|
|
168
|
+
|
|
169
|
+
def _onresponse(self, data: Any) -> None:
|
|
170
|
+
if isinstance(data, dict):
|
|
171
|
+
entries = data.get("data") or []
|
|
172
|
+
else:
|
|
173
|
+
entries = data
|
|
174
|
+
|
|
175
|
+
self._update(entries)
|
|
176
|
+
|
|
177
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
178
|
+
data = msg.get("data")
|
|
179
|
+
entries: list[dict[str, Any]] = []
|
|
180
|
+
if isinstance(data, list):
|
|
181
|
+
entries = data
|
|
182
|
+
elif isinstance(data, dict):
|
|
183
|
+
entries = data.get("data") or data.get("tickers") or []
|
|
184
|
+
|
|
185
|
+
items: list[dict[str, Any]] = []
|
|
186
|
+
for entry in entries:
|
|
187
|
+
if not isinstance(entry, dict):
|
|
188
|
+
continue
|
|
189
|
+
normalized = self._normalize(entry)
|
|
190
|
+
if normalized:
|
|
191
|
+
items.append(normalized)
|
|
192
|
+
|
|
193
|
+
if not items:
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
instruments = [{"instrument": item["instrument"]} for item in items]
|
|
197
|
+
self._delete(instruments)
|
|
198
|
+
self._insert(items)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Orders(DataStore):
|
|
202
|
+
"""CoinW 当前订单数据存储。"""
|
|
203
|
+
|
|
204
|
+
_KEYS = ["id"]
|
|
205
|
+
|
|
206
|
+
@staticmethod
|
|
207
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
208
|
+
order_id = entry.get("id")
|
|
209
|
+
if order_id is None:
|
|
210
|
+
return None
|
|
211
|
+
normalized = dict(entry)
|
|
212
|
+
normalized["id"] = str(order_id)
|
|
213
|
+
return normalized
|
|
214
|
+
|
|
215
|
+
def _onresponse(self, data: Any) -> None:
|
|
216
|
+
payload = []
|
|
217
|
+
if isinstance(data, dict):
|
|
218
|
+
inner = data.get("data")
|
|
219
|
+
if isinstance(inner, dict):
|
|
220
|
+
payload = inner.get("rows") or []
|
|
221
|
+
elif isinstance(inner, list):
|
|
222
|
+
payload = inner
|
|
223
|
+
elif isinstance(data, list):
|
|
224
|
+
payload = data
|
|
225
|
+
|
|
226
|
+
items: list[dict[str, Any]] = []
|
|
227
|
+
for entry in payload or []:
|
|
228
|
+
if not isinstance(entry, dict):
|
|
229
|
+
continue
|
|
230
|
+
normalized = self._normalize(entry)
|
|
231
|
+
if normalized:
|
|
232
|
+
items.append(normalized)
|
|
233
|
+
|
|
234
|
+
self._clear()
|
|
235
|
+
if items:
|
|
236
|
+
self._insert(items)
|
|
237
|
+
|
|
238
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
239
|
+
data = msg.get("data")
|
|
240
|
+
if isinstance(data, dict) and data.get("result") is not None:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
entries: list[dict[str, Any]] = []
|
|
244
|
+
if isinstance(data, list):
|
|
245
|
+
entries = data
|
|
246
|
+
elif isinstance(data, dict):
|
|
247
|
+
entries = data.get("rows") or data.get("data") or []
|
|
248
|
+
|
|
249
|
+
if not entries:
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
to_insert: list[dict[str, Any]] = []
|
|
253
|
+
to_delete: list[dict[str, Any]] = []
|
|
254
|
+
|
|
255
|
+
for entry in entries:
|
|
256
|
+
if not isinstance(entry, dict):
|
|
257
|
+
continue
|
|
258
|
+
normalized = self._normalize(entry)
|
|
259
|
+
if not normalized:
|
|
260
|
+
continue
|
|
261
|
+
|
|
262
|
+
status = str(normalized.get("status") or "").lower()
|
|
263
|
+
order_status = str(normalized.get("orderStatus") or "").lower()
|
|
264
|
+
remove = status in {"close", "cancel", "canceled"} or order_status in {
|
|
265
|
+
"finish",
|
|
266
|
+
"cancel",
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# query = {"id": normalized["id"]}
|
|
270
|
+
to_delete.append(normalized)
|
|
271
|
+
if not remove:
|
|
272
|
+
to_insert.append(normalized)
|
|
273
|
+
|
|
274
|
+
if to_delete:
|
|
275
|
+
self._delete(to_delete)
|
|
276
|
+
if to_insert:
|
|
277
|
+
self._insert(to_insert)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
class Position(DataStore):
|
|
281
|
+
"""CoinW 当前持仓数据存储。"""
|
|
282
|
+
|
|
283
|
+
_KEYS = ["openId"]
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _onresponse(self, data: Any) -> None:
|
|
287
|
+
payload = []
|
|
288
|
+
if isinstance(data, dict):
|
|
289
|
+
payload = data.get("data") or []
|
|
290
|
+
elif isinstance(data, list):
|
|
291
|
+
payload = data
|
|
292
|
+
|
|
293
|
+
items: list[dict[str, Any]] = []
|
|
294
|
+
for entry in payload or []:
|
|
295
|
+
if not isinstance(entry, dict):
|
|
296
|
+
continue
|
|
297
|
+
entry['openId'] = str(entry.get("id"))
|
|
298
|
+
items.append(entry)
|
|
299
|
+
|
|
300
|
+
self._clear()
|
|
301
|
+
if items:
|
|
302
|
+
self._insert(items)
|
|
303
|
+
|
|
304
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
305
|
+
data = msg.get("data")
|
|
306
|
+
|
|
307
|
+
if isinstance(data, dict) and data.get("result") is not None:
|
|
308
|
+
return
|
|
309
|
+
|
|
310
|
+
entries: list[dict[str, Any]] = []
|
|
311
|
+
if isinstance(data, list):
|
|
312
|
+
entries = data
|
|
313
|
+
elif isinstance(data, dict):
|
|
314
|
+
entries = data.get("rows") or data.get("data") or []
|
|
315
|
+
|
|
316
|
+
if not entries:
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
to_insert: list[dict[str, Any]] = []
|
|
320
|
+
to_update: list[dict[str, Any]] = []
|
|
321
|
+
to_delete: list[dict[str, Any]] = []
|
|
322
|
+
|
|
323
|
+
for entry in entries:
|
|
324
|
+
if not isinstance(entry, dict):
|
|
325
|
+
continue
|
|
326
|
+
normalized = entry
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
if normalized.get("status") == 'close':
|
|
330
|
+
to_delete.append(normalized)
|
|
331
|
+
continue
|
|
332
|
+
|
|
333
|
+
if self.find(normalized):
|
|
334
|
+
to_update.append(normalized)
|
|
335
|
+
else:
|
|
336
|
+
to_insert.append(normalized)
|
|
337
|
+
|
|
338
|
+
if to_delete:
|
|
339
|
+
self._delete(to_delete)
|
|
340
|
+
if to_update:
|
|
341
|
+
self._update(to_update)
|
|
342
|
+
if to_insert:
|
|
343
|
+
self._insert(to_insert)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class Balance(DataStore):
|
|
347
|
+
"""CoinW 合约账户资产数据存储。"""
|
|
348
|
+
|
|
349
|
+
_KEYS = ["currency"]
|
|
350
|
+
|
|
351
|
+
@staticmethod
|
|
352
|
+
def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any]:
|
|
353
|
+
currency = "USDT"
|
|
354
|
+
normalized = {
|
|
355
|
+
"currency": currency,
|
|
356
|
+
"availableMargin": entry.get("availableMargin"),
|
|
357
|
+
"availableUsdt": entry.get("availableUsdt"),
|
|
358
|
+
"almightyGold": entry.get("almightyGold"),
|
|
359
|
+
"alMargin": entry.get("alMargin"),
|
|
360
|
+
"alFreeze": entry.get("alFreeze"),
|
|
361
|
+
"time": entry.get("time"),
|
|
362
|
+
"userId": entry.get("userId"),
|
|
363
|
+
}
|
|
364
|
+
if "available" not in normalized:
|
|
365
|
+
normalized["available"] = entry.get("availableUsdt")
|
|
366
|
+
normalized["availableMargin"] = entry.get("availableMargin")
|
|
367
|
+
normalized["margin"] = entry.get("alMargin")
|
|
368
|
+
normalized["freeze"] = entry.get("alFreeze")
|
|
369
|
+
return {k: v for k, v in normalized.items() if v is not None}
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def _normalize_ws(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
373
|
+
currency = entry.get("currency")
|
|
374
|
+
if not currency:
|
|
375
|
+
return None
|
|
376
|
+
currency_str = str(currency).upper()
|
|
377
|
+
normalized = dict(entry)
|
|
378
|
+
normalized["currency"] = currency_str
|
|
379
|
+
# 对齐 REST 字段
|
|
380
|
+
if "availableUsdt" not in normalized and "available" in normalized:
|
|
381
|
+
normalized["availableUsdt"] = normalized["available"]
|
|
382
|
+
if "alMargin" not in normalized and "margin" in normalized:
|
|
383
|
+
normalized["alMargin"] = normalized["margin"]
|
|
384
|
+
if "alFreeze" not in normalized and "freeze" in normalized:
|
|
385
|
+
normalized["alFreeze"] = normalized["freeze"]
|
|
386
|
+
return normalized
|
|
387
|
+
|
|
388
|
+
def _onresponse(self, data: Any) -> None:
|
|
389
|
+
entry = None
|
|
390
|
+
if isinstance(data, dict):
|
|
391
|
+
entry = data.get("data")
|
|
392
|
+
if not isinstance(entry, dict):
|
|
393
|
+
entry = {}
|
|
394
|
+
|
|
395
|
+
self._clear()
|
|
396
|
+
normalized = self._normalize_rest(entry)
|
|
397
|
+
self._insert([normalized])
|
|
398
|
+
|
|
399
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
400
|
+
data = msg.get("data")
|
|
401
|
+
if isinstance(data, dict) and data.get("result") is not None:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
entries: list[dict[str, Any]] = []
|
|
405
|
+
if isinstance(data, list):
|
|
406
|
+
entries = data
|
|
407
|
+
elif isinstance(data, dict):
|
|
408
|
+
entries = data.get("rows") or data.get("data") or []
|
|
409
|
+
|
|
410
|
+
if not entries:
|
|
411
|
+
return
|
|
412
|
+
|
|
413
|
+
normalized_items: list[dict[str, Any]] = []
|
|
414
|
+
for entry in entries:
|
|
415
|
+
if not isinstance(entry, dict):
|
|
416
|
+
continue
|
|
417
|
+
normalized = self._normalize_ws(entry)
|
|
418
|
+
if normalized:
|
|
419
|
+
normalized_items.append(normalized)
|
|
420
|
+
|
|
421
|
+
if not normalized_items:
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
currencies = [{"currency": item["currency"]} for item in normalized_items]
|
|
425
|
+
self._delete(currencies)
|
|
426
|
+
self._insert(normalized_items)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
class CoinwFuturesDataStore(DataStoreCollection):
|
|
430
|
+
"""CoinW 合约交易 DataStoreCollection。
|
|
431
|
+
|
|
432
|
+
- REST: https://api.coinw.com/v1/perpum/instruments
|
|
433
|
+
- WebSocket: wss://ws.futurescw.com/perpum (depth)
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
def _init(self) -> None:
|
|
437
|
+
self._create("book", datastore_class=Book)
|
|
438
|
+
self._create("detail", datastore_class=Detail)
|
|
439
|
+
self._create("ticker", datastore_class=Ticker)
|
|
440
|
+
self._create("orders", datastore_class=Orders)
|
|
441
|
+
self._create("position", datastore_class=Position)
|
|
442
|
+
self._create("balance", datastore_class=Balance)
|
|
443
|
+
|
|
444
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
445
|
+
msg_type = msg.get("type")
|
|
446
|
+
# print(msg)
|
|
447
|
+
if msg_type == "depth":
|
|
448
|
+
self.book._on_message(msg)
|
|
449
|
+
elif msg_type == "order":
|
|
450
|
+
self.orders._on_message(msg)
|
|
451
|
+
elif msg_type == "position" or msg_type == "position_change":
|
|
452
|
+
self.position._on_message(msg)
|
|
453
|
+
elif msg_type == "assets":
|
|
454
|
+
self.balance._on_message(msg)
|
|
455
|
+
elif msg_type == "ticker":
|
|
456
|
+
self.ticker._on_message(msg)
|
|
457
|
+
|
|
458
|
+
async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
|
|
459
|
+
for fut in asyncio.as_completed(aws):
|
|
460
|
+
res = await fut
|
|
461
|
+
data = await res.json()
|
|
462
|
+
if res.url.path == "/v1/perpum/instruments":
|
|
463
|
+
self.detail._onresponse(data)
|
|
464
|
+
elif res.url.path == "/v1/perpumPublic/tickers":
|
|
465
|
+
self.ticker._onresponse(data)
|
|
466
|
+
elif res.url.path == "/v1/perpum/orders/open":
|
|
467
|
+
self.orders._onresponse(data)
|
|
468
|
+
elif res.url.path == "/v1/perpum/positions/all":
|
|
469
|
+
self.position._onresponse(data)
|
|
470
|
+
elif res.url.path == "/v1/perpum/account/getUserAssets":
|
|
471
|
+
self.balance._onresponse(data)
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def book(self) -> Book:
|
|
475
|
+
"""订单簿深度数据流。
|
|
476
|
+
|
|
477
|
+
数据来源:
|
|
478
|
+
- WebSocket: ``type == "depth"`` (参考 https://www.coinw.com/api-doc/futures-trading/market/subscribe-order-book)
|
|
479
|
+
|
|
480
|
+
数据结构(节选)::
|
|
481
|
+
|
|
482
|
+
{
|
|
483
|
+
"s": "BTC",
|
|
484
|
+
"S": "a", # 卖单
|
|
485
|
+
"p": "95640.3",
|
|
486
|
+
"q": "0.807"
|
|
487
|
+
}
|
|
488
|
+
"""
|
|
489
|
+
|
|
490
|
+
return self._get("book", Book)
|
|
491
|
+
|
|
492
|
+
@property
|
|
493
|
+
def detail(self) -> Detail:
|
|
494
|
+
"""合约基础信息数据流。
|
|
495
|
+
|
|
496
|
+
响应示例(节选):
|
|
497
|
+
|
|
498
|
+
.. code:: json
|
|
499
|
+
|
|
500
|
+
{
|
|
501
|
+
"base": "btc",
|
|
502
|
+
"closeSpread": 0.0002,
|
|
503
|
+
"commissionRate": 0.0006,
|
|
504
|
+
"configBo": {
|
|
505
|
+
"margins": {
|
|
506
|
+
"100": 0.075,
|
|
507
|
+
"5": 0.00375,
|
|
508
|
+
"50": 0.0375,
|
|
509
|
+
"20": 0.015,
|
|
510
|
+
"10": 0.0075
|
|
511
|
+
},
|
|
512
|
+
"simulatedMargins": {
|
|
513
|
+
"5": 0.00375,
|
|
514
|
+
"20": 0.015,
|
|
515
|
+
"10": 0.0075
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
"createdDate": 1548950400000,
|
|
519
|
+
"defaultLeverage": 20,
|
|
520
|
+
"defaultStopLossRate": 0.99,
|
|
521
|
+
"defaultStopProfitRate": 100,
|
|
522
|
+
"depthPrecision": "0.1,1,10,50,100",
|
|
523
|
+
"iconUrl": "https://hkto-prod.oss-accelerate.aliyuncs.com/4dfca512e957e14f05da07751a96061cf4bfd5df438504f65287fa0a8c3cadb6.svg",
|
|
524
|
+
"id": 1,
|
|
525
|
+
"indexId": 1,
|
|
526
|
+
"leverage": [
|
|
527
|
+
5,
|
|
528
|
+
10,
|
|
529
|
+
20,
|
|
530
|
+
50,
|
|
531
|
+
100,
|
|
532
|
+
125,
|
|
533
|
+
200
|
|
534
|
+
],
|
|
535
|
+
"makerFee": "0.0001",
|
|
536
|
+
"maxLeverage": 200,
|
|
537
|
+
"maxPosition": 20000,
|
|
538
|
+
"minLeverage": 1,
|
|
539
|
+
"minSize": 1,
|
|
540
|
+
"name": "BTC",
|
|
541
|
+
"oneLotMargin": 1,
|
|
542
|
+
"oneLotSize": 0.001,
|
|
543
|
+
"oneMaxPosition": 15000,
|
|
544
|
+
"openSpread": 0.0003,
|
|
545
|
+
"orderLimitMaxRate": 0.05,
|
|
546
|
+
"orderLimitMinRate": 0.05,
|
|
547
|
+
"orderMarketLimitAmount": 10,
|
|
548
|
+
"orderPlanLimitAmount": 30,
|
|
549
|
+
"partitionIds": "2013,2011",
|
|
550
|
+
"platform": 0,
|
|
551
|
+
"pricePrecision": 1,
|
|
552
|
+
"quote": "usdt",
|
|
553
|
+
"selected": 0,
|
|
554
|
+
"settledAt": 1761062400000,
|
|
555
|
+
"settledPeriod": 8,
|
|
556
|
+
"settlementRate": 0.0004,
|
|
557
|
+
"sort": 1,
|
|
558
|
+
"status": "online",
|
|
559
|
+
"stopCrossPositionRate": 0.1,
|
|
560
|
+
"stopSurplusRate": 0.01,
|
|
561
|
+
"takerFee": "0.0006",
|
|
562
|
+
"updatedDate": 1752040118000,
|
|
563
|
+
"symbol": "BTC_USDT",
|
|
564
|
+
"tick_size": 1.0,
|
|
565
|
+
"step_size": 0.001
|
|
566
|
+
}
|
|
567
|
+
"""
|
|
568
|
+
|
|
569
|
+
return self._get("detail", Detail)
|
|
570
|
+
|
|
571
|
+
@property
|
|
572
|
+
def ticker(self) -> Ticker:
|
|
573
|
+
"""24小时交易摘要数据流。
|
|
574
|
+
|
|
575
|
+
.. code:: json
|
|
576
|
+
|
|
577
|
+
{
|
|
578
|
+
'fair_price': 97072.4,
|
|
579
|
+
'max_leverage': 125,
|
|
580
|
+
'total_volume': 0.003,
|
|
581
|
+
'price_coin': 'btc',
|
|
582
|
+
'contract_id': 1,
|
|
583
|
+
'base_coin': 'btc',
|
|
584
|
+
'high': 98001.5,
|
|
585
|
+
'rise_fall_rate': 0.012275,
|
|
586
|
+
'low': 95371.4,
|
|
587
|
+
'name': 'BTCUSDT',
|
|
588
|
+
'contract_size': 0.001,
|
|
589
|
+
'quote_coin': 'usdt',
|
|
590
|
+
'last_price': 97072.4
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
"""
|
|
594
|
+
return self._get("ticker", Ticker)
|
|
595
|
+
|
|
596
|
+
@property
|
|
597
|
+
def orders(self) -> Orders:
|
|
598
|
+
"""当前订单数据流。
|
|
599
|
+
|
|
600
|
+
数据来源:
|
|
601
|
+
- REST: ``GET /v1/perpum/orders/open``
|
|
602
|
+
- WebSocket: ``type == "order"``
|
|
603
|
+
|
|
604
|
+
数据结构(节选)::
|
|
605
|
+
|
|
606
|
+
{
|
|
607
|
+
"currentPiece": "1",
|
|
608
|
+
"leverage": "50",
|
|
609
|
+
"originalType": "plan",
|
|
610
|
+
"processStatus": 0,
|
|
611
|
+
"contractType": 1,
|
|
612
|
+
"frozenFee": "0",
|
|
613
|
+
"openPrice": "175",
|
|
614
|
+
"orderStatus": "unFinish",
|
|
615
|
+
"instrument": "SOL",
|
|
616
|
+
"quantityUnit": 1,
|
|
617
|
+
"source": "web",
|
|
618
|
+
"updatedDate": 1761109078404,
|
|
619
|
+
"positionModel": 1,
|
|
620
|
+
"posType": "plan",
|
|
621
|
+
"baseSize": "0.1",
|
|
622
|
+
"quote": "usdt",
|
|
623
|
+
"liquidateBy": "manual",
|
|
624
|
+
"makerFee": "0.0001",
|
|
625
|
+
"totalPiece": "1",
|
|
626
|
+
"tradePiece": "0",
|
|
627
|
+
"orderPrice": "175",
|
|
628
|
+
"id": "33309055657317395",
|
|
629
|
+
"direction": "long",
|
|
630
|
+
"margin": "0.35",
|
|
631
|
+
"indexPrice": "185.68",
|
|
632
|
+
"quantity": "1",
|
|
633
|
+
"takerFee": "0.0006",
|
|
634
|
+
"userId": "1757458",
|
|
635
|
+
"cancelPiece": "0",
|
|
636
|
+
"createdDate": 1761109078404,
|
|
637
|
+
"positionMargin": "0.35",
|
|
638
|
+
"base": "sol",
|
|
639
|
+
"status": "open"
|
|
640
|
+
}
|
|
641
|
+
"""
|
|
642
|
+
|
|
643
|
+
return self._get("orders", Orders)
|
|
644
|
+
|
|
645
|
+
@property
|
|
646
|
+
def position(self) -> Position:
|
|
647
|
+
"""当前持仓数据流。
|
|
648
|
+
|
|
649
|
+
数据来源:
|
|
650
|
+
- REST: ``GET /v1/perpum/positions``
|
|
651
|
+
- WebSocket: ``type == "position"``
|
|
652
|
+
|
|
653
|
+
.. code:: json
|
|
654
|
+
|
|
655
|
+
{
|
|
656
|
+
"currentPiece": "0",
|
|
657
|
+
"isProfession": 0,
|
|
658
|
+
"leverage": "10",
|
|
659
|
+
"originalType": "execute",
|
|
660
|
+
"orderId": "33309059291614824",
|
|
661
|
+
"contractType": 1,
|
|
662
|
+
"openId": "2435521222638707873",
|
|
663
|
+
"fee": "0.00020724",
|
|
664
|
+
"openPrice": "0.3456",
|
|
665
|
+
"orderStatus": "finish",
|
|
666
|
+
"instrument": "JUP",
|
|
667
|
+
"quantityUnit": 1,
|
|
668
|
+
"source": "api",
|
|
669
|
+
"updatedDate": 1761192795412,
|
|
670
|
+
"positionModel": 1,
|
|
671
|
+
"feeRate": "0.0006",
|
|
672
|
+
"netProfit": "-0.00040724",
|
|
673
|
+
"baseSize": "1",
|
|
674
|
+
"quote": "usdt",
|
|
675
|
+
"liquidateBy": "manual",
|
|
676
|
+
"totalPiece": "1",
|
|
677
|
+
"orderPrice": "0",
|
|
678
|
+
"id": "23469279597150213",
|
|
679
|
+
"fundingSettle": "0",
|
|
680
|
+
"direction": "long",
|
|
681
|
+
"margin": "0.03435264",
|
|
682
|
+
"takerMaker": 1,
|
|
683
|
+
"indexPrice": "0.3455",
|
|
684
|
+
"quantity": "0.03456",
|
|
685
|
+
"userId": "1757458",
|
|
686
|
+
"closedPiece": "1",
|
|
687
|
+
"createdDate": 1761192793000,
|
|
688
|
+
"hedgeId": "23469279597150214",
|
|
689
|
+
"closePrice": "0.3454",
|
|
690
|
+
"positionMargin": "0.03435264",
|
|
691
|
+
"base": "jup",
|
|
692
|
+
"realPrice": "0.3454",
|
|
693
|
+
"status": "close"
|
|
694
|
+
}
|
|
695
|
+
"""
|
|
696
|
+
|
|
697
|
+
return self._get("position", Position)
|
|
698
|
+
|
|
699
|
+
@property
|
|
700
|
+
def balance(self) -> Balance:
|
|
701
|
+
"""合约账户资产数据流。
|
|
702
|
+
|
|
703
|
+
数据来源:
|
|
704
|
+
- REST: ``GET /v1/perpum/account/getUserAssets``
|
|
705
|
+
- WebSocket: ``type == "assets"``
|
|
706
|
+
|
|
707
|
+
.. code:: json
|
|
708
|
+
|
|
709
|
+
{
|
|
710
|
+
"currency": "USDT",
|
|
711
|
+
"availableMargin": 0.0,
|
|
712
|
+
"availableUsdt": 0,
|
|
713
|
+
"almightyGold": 0.0,
|
|
714
|
+
"alMargin": 0,
|
|
715
|
+
"alFreeze": 0,
|
|
716
|
+
"time": 1761055905797,
|
|
717
|
+
"userId": 1757458,
|
|
718
|
+
"available": 0,
|
|
719
|
+
"margin": 0,
|
|
720
|
+
"freeze": 0
|
|
721
|
+
}
|
|
722
|
+
"""
|
|
723
|
+
|
|
724
|
+
return self._get("balance", Balance)
|