hyperquant 1.48__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/__init__.py +8 -0
- hyperquant/broker/auth.py +972 -0
- hyperquant/broker/bitget.py +311 -0
- hyperquant/broker/bitmart.py +720 -0
- hyperquant/broker/coinw.py +487 -0
- hyperquant/broker/deepcoin.py +651 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/hyperliquid.py +570 -0
- hyperquant/broker/lbank.py +661 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/hpstore.py +252 -0
- hyperquant/broker/lib/hyper_types.py +48 -0
- hyperquant/broker/lib/polymarket/ctfAbi.py +721 -0
- hyperquant/broker/lib/polymarket/safeAbi.py +1138 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/lighter.py +679 -0
- hyperquant/broker/models/apexpro.py +150 -0
- hyperquant/broker/models/bitget.py +359 -0
- hyperquant/broker/models/bitmart.py +635 -0
- hyperquant/broker/models/coinw.py +724 -0
- hyperquant/broker/models/deepcoin.py +809 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/lbank.py +557 -0
- hyperquant/broker/models/lighter.py +868 -0
- hyperquant/broker/models/ourbit.py +1155 -0
- hyperquant/broker/models/polymarket.py +1071 -0
- hyperquant/broker/ourbit.py +550 -0
- hyperquant/broker/polymarket.py +2399 -0
- hyperquant/broker/ws.py +132 -0
- hyperquant/core.py +513 -0
- hyperquant/datavison/_util.py +18 -0
- hyperquant/datavison/binance.py +111 -0
- hyperquant/datavison/coinglass.py +237 -0
- hyperquant/datavison/okx.py +177 -0
- hyperquant/db.py +191 -0
- hyperquant/draw.py +1200 -0
- hyperquant/logkit.py +205 -0
- hyperquant/notikit.py +124 -0
- hyperquant-1.48.dist-info/METADATA +32 -0
- hyperquant-1.48.dist-info/RECORD +42 -0
- hyperquant-1.48.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import TYPE_CHECKING, Any, Awaitable
|
|
5
|
+
|
|
6
|
+
import aiohttp
|
|
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
|
+
_DEFAULT_QUOTE = "USDT"
|
|
15
|
+
_DEFAULT_INST_TYPE = "SWAP"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _extract_base(value: Any) -> str | None:
|
|
19
|
+
"""Extract the base currency (e.g., BTC) from various identifier forms."""
|
|
20
|
+
if not value:
|
|
21
|
+
return None
|
|
22
|
+
text = str(value).strip().upper()
|
|
23
|
+
if not text:
|
|
24
|
+
return None
|
|
25
|
+
for sep in ("/", "_"):
|
|
26
|
+
text = text.replace(sep, "-")
|
|
27
|
+
if text.endswith("-SWAP"):
|
|
28
|
+
text = text[:-5]
|
|
29
|
+
parts = text.split("-")
|
|
30
|
+
candidate = parts[0] if parts else text
|
|
31
|
+
if candidate.endswith(_DEFAULT_QUOTE):
|
|
32
|
+
candidate = candidate[: -len(_DEFAULT_QUOTE)]
|
|
33
|
+
candidate = candidate.replace(" ", "")
|
|
34
|
+
return candidate or None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ensure_identifiers(entry: dict[str, Any]) -> dict[str, Any]:
|
|
38
|
+
"""
|
|
39
|
+
Ensure symbol is formatted as BTCUSDT and instId as BTC-USDT-SWAP
|
|
40
|
+
assuming DeepCoin swap instruments quote in USDT.
|
|
41
|
+
"""
|
|
42
|
+
base = (
|
|
43
|
+
entry.get("baseCcy")
|
|
44
|
+
or entry.get("base")
|
|
45
|
+
or _extract_base(entry.get("instId"))
|
|
46
|
+
or _extract_base(entry.get("I"))
|
|
47
|
+
or _extract_base(entry.get("symbol"))
|
|
48
|
+
or _extract_base(entry.get("s"))
|
|
49
|
+
)
|
|
50
|
+
if not base:
|
|
51
|
+
return entry
|
|
52
|
+
|
|
53
|
+
base = str(base).upper()
|
|
54
|
+
symbol = f"{base}{_DEFAULT_QUOTE}"
|
|
55
|
+
inst_id = f"{base}-{_DEFAULT_QUOTE}-{_DEFAULT_INST_TYPE}"
|
|
56
|
+
|
|
57
|
+
entry["s"] = symbol
|
|
58
|
+
entry["symbol"] = symbol
|
|
59
|
+
entry["instId"] = inst_id
|
|
60
|
+
entry["I"] = inst_id
|
|
61
|
+
return entry
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_ticker(msg:dict) -> dict | None:
|
|
65
|
+
r = msg.get("r", [])
|
|
66
|
+
if len(r) == 0:
|
|
67
|
+
return None
|
|
68
|
+
ticker = r[0].get('d')
|
|
69
|
+
if ticker:
|
|
70
|
+
_ensure_identifiers(ticker)
|
|
71
|
+
return ticker
|
|
72
|
+
|
|
73
|
+
class Detail(DataStore):
|
|
74
|
+
_KEYS = ["s"]
|
|
75
|
+
|
|
76
|
+
def _on_response(self, msg: dict[str, Any]) -> None:
|
|
77
|
+
data = msg.get('data', [])
|
|
78
|
+
# 展开data 新增tick_size 同 tickSize 字段 step_size 同 lotSize 字段
|
|
79
|
+
for item in data:
|
|
80
|
+
_ensure_identifiers(item)
|
|
81
|
+
if 'tickSize' in item:
|
|
82
|
+
item['tick_size'] = item['tickSize']
|
|
83
|
+
elif 'tickSz' in item:
|
|
84
|
+
item['tick_size'] = item['tickSz']
|
|
85
|
+
if 'lotSize' in item:
|
|
86
|
+
item['step_size'] = item['lotSize']
|
|
87
|
+
elif 'lotSz' in item:
|
|
88
|
+
item['step_size'] = item['lotSz']
|
|
89
|
+
|
|
90
|
+
self._update(data)
|
|
91
|
+
|
|
92
|
+
class Ticker(DataStore):
|
|
93
|
+
_KEYS = ["s"]
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
97
|
+
normalized_entry = _ensure_identifiers(dict(entry))
|
|
98
|
+
symbol = normalized_entry.get("s")
|
|
99
|
+
if not symbol:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
normalized = dict(normalized_entry)
|
|
103
|
+
|
|
104
|
+
bid = entry.get("bidPx") or entry.get("BP1")
|
|
105
|
+
ask = entry.get("askPx") or entry.get("AP1")
|
|
106
|
+
try:
|
|
107
|
+
if bid is not None:
|
|
108
|
+
normalized["BP1"] = float(bid)
|
|
109
|
+
except (TypeError, ValueError):
|
|
110
|
+
pass
|
|
111
|
+
try:
|
|
112
|
+
if ask is not None:
|
|
113
|
+
normalized["AP1"] = float(ask)
|
|
114
|
+
except (TypeError, ValueError):
|
|
115
|
+
pass
|
|
116
|
+
|
|
117
|
+
return normalized
|
|
118
|
+
|
|
119
|
+
def _on_response(self, msg: dict[str, Any]) -> None:
|
|
120
|
+
data = msg.get("data") or []
|
|
121
|
+
items: list[dict[str, Any]] = []
|
|
122
|
+
for entry in data:
|
|
123
|
+
if not isinstance(entry, dict):
|
|
124
|
+
continue
|
|
125
|
+
normalized = self._normalize(entry)
|
|
126
|
+
if normalized:
|
|
127
|
+
items.append(normalized)
|
|
128
|
+
|
|
129
|
+
self._clear()
|
|
130
|
+
if items:
|
|
131
|
+
self._insert(items)
|
|
132
|
+
|
|
133
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
134
|
+
ticker = _get_ticker(msg)
|
|
135
|
+
if ticker:
|
|
136
|
+
self._update([ticker])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Book(DataStore):
|
|
140
|
+
"""用ticker数据构建的深度数据(book_best)"""
|
|
141
|
+
|
|
142
|
+
_KEYS = ["s", "S"]
|
|
143
|
+
|
|
144
|
+
def _init(self) -> None:
|
|
145
|
+
self.limit: int | None = None
|
|
146
|
+
self.id_to_symbol: dict[str, str] = {}
|
|
147
|
+
self._state: dict[str, dict[str, dict[float, float]]] = {}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
151
|
+
ticker = _get_ticker(msg)
|
|
152
|
+
if not ticker:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
symbol = ticker.get("s")
|
|
156
|
+
self._update([
|
|
157
|
+
{
|
|
158
|
+
's': symbol,
|
|
159
|
+
'S': 'b',
|
|
160
|
+
'p': float(ticker.get('BP1', 0)),
|
|
161
|
+
'q': 0
|
|
162
|
+
},{
|
|
163
|
+
's': symbol,
|
|
164
|
+
'S': 'a',
|
|
165
|
+
'p': float(ticker.get('AP1', 0)),
|
|
166
|
+
'q': 0
|
|
167
|
+
}
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class Balance(DataStore):
|
|
172
|
+
"""资金余额数据。"""
|
|
173
|
+
|
|
174
|
+
_KEYS = ["ccy"]
|
|
175
|
+
|
|
176
|
+
@staticmethod
|
|
177
|
+
def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
178
|
+
ccy = entry.get("ccy")
|
|
179
|
+
if not ccy:
|
|
180
|
+
return None
|
|
181
|
+
normalized = {
|
|
182
|
+
"ccy": str(ccy),
|
|
183
|
+
"bal": entry.get("bal"),
|
|
184
|
+
"availBal": entry.get("availBal"),
|
|
185
|
+
"frozenBal": entry.get("frozenBal"),
|
|
186
|
+
}
|
|
187
|
+
if entry.get("withdrawable") is not None:
|
|
188
|
+
normalized["withdrawable"] = entry.get("withdrawable")
|
|
189
|
+
return normalized
|
|
190
|
+
|
|
191
|
+
@staticmethod
|
|
192
|
+
def _normalize_ws(data: dict[str, Any]) -> dict[str, Any] | None:
|
|
193
|
+
if not isinstance(data, dict):
|
|
194
|
+
return None
|
|
195
|
+
mapping = {
|
|
196
|
+
"A": "accountId",
|
|
197
|
+
"B": "bal",
|
|
198
|
+
"C": "ccy",
|
|
199
|
+
"M": "memberId",
|
|
200
|
+
"W": "withdrawable",
|
|
201
|
+
"a": "availBal",
|
|
202
|
+
"u": "useMargin",
|
|
203
|
+
"c": "closeProfit",
|
|
204
|
+
"FF": "frozenFee",
|
|
205
|
+
"FM": "frozenMargin",
|
|
206
|
+
}
|
|
207
|
+
normalized: dict[str, Any] = {}
|
|
208
|
+
for key, value in data.items():
|
|
209
|
+
target = mapping.get(key)
|
|
210
|
+
if target:
|
|
211
|
+
normalized[target] = value
|
|
212
|
+
else:
|
|
213
|
+
normalized[key] = value
|
|
214
|
+
if "ccy" not in normalized and "C" in data:
|
|
215
|
+
normalized["ccy"] = data["C"]
|
|
216
|
+
if "bal" not in normalized and "B" in data:
|
|
217
|
+
normalized["bal"] = data["B"]
|
|
218
|
+
if not normalized.get("ccy"):
|
|
219
|
+
return None
|
|
220
|
+
return normalized
|
|
221
|
+
|
|
222
|
+
def _on_response(self, msg: dict[str, Any]) -> None:
|
|
223
|
+
data = msg.get("data") or []
|
|
224
|
+
items: list[dict[str, Any]] = []
|
|
225
|
+
for entry in data:
|
|
226
|
+
if not isinstance(entry, dict):
|
|
227
|
+
continue
|
|
228
|
+
normalized = self._normalize_rest(entry)
|
|
229
|
+
if normalized:
|
|
230
|
+
items.append(normalized)
|
|
231
|
+
|
|
232
|
+
self._clear()
|
|
233
|
+
if items:
|
|
234
|
+
self._insert(items)
|
|
235
|
+
|
|
236
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
237
|
+
results = msg.get("result") or []
|
|
238
|
+
for item in results:
|
|
239
|
+
data = item.get("data") if isinstance(item, dict) else None
|
|
240
|
+
normalized = self._normalize_ws(data or {})
|
|
241
|
+
if not normalized:
|
|
242
|
+
continue
|
|
243
|
+
key = {"ccy": str(normalized["ccy"])}
|
|
244
|
+
if self.get(key):
|
|
245
|
+
self._update([normalized])
|
|
246
|
+
else:
|
|
247
|
+
self._insert([normalized])
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Position(DataStore):
|
|
251
|
+
"""仓位数据。"""
|
|
252
|
+
|
|
253
|
+
_KEYS = ["instId", "posSide"]
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
257
|
+
normalized = _ensure_identifiers(dict(entry))
|
|
258
|
+
inst_id = normalized.get("instId")
|
|
259
|
+
pos_side = normalized.get("posSide")
|
|
260
|
+
if not inst_id or pos_side is None:
|
|
261
|
+
return None
|
|
262
|
+
normalized["instId"] = str(inst_id)
|
|
263
|
+
normalized["posSide"] = str(pos_side)
|
|
264
|
+
return normalized
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _normalize_ws(data: dict[str, Any]) -> dict[str, Any] | None:
|
|
268
|
+
if not isinstance(data, dict):
|
|
269
|
+
return None
|
|
270
|
+
mapping = {
|
|
271
|
+
"A": "accountId",
|
|
272
|
+
"I": "instId",
|
|
273
|
+
"M": "memberId",
|
|
274
|
+
"OP": "avgPx",
|
|
275
|
+
"Po": "pos",
|
|
276
|
+
"U": "updateTime",
|
|
277
|
+
"p": "posSide",
|
|
278
|
+
"u": "useMargin",
|
|
279
|
+
"c": "closeProfit",
|
|
280
|
+
"l": "lever",
|
|
281
|
+
"i": "isCrossMargin",
|
|
282
|
+
}
|
|
283
|
+
normalized: dict[str, Any] = {}
|
|
284
|
+
for key, value in data.items():
|
|
285
|
+
target = mapping.get(key)
|
|
286
|
+
if target:
|
|
287
|
+
normalized[target] = value
|
|
288
|
+
else:
|
|
289
|
+
normalized[key] = value
|
|
290
|
+
normalized = _ensure_identifiers(normalized)
|
|
291
|
+
if "posSide" not in normalized and "p" in data:
|
|
292
|
+
normalized["posSide"] = str(data["p"])
|
|
293
|
+
|
|
294
|
+
if not normalized.get("instId") or normalized.get("posSide") is None:
|
|
295
|
+
return None
|
|
296
|
+
return normalized
|
|
297
|
+
|
|
298
|
+
def _on_response(self, msg: dict[str, Any]) -> None:
|
|
299
|
+
data = msg.get("data") or []
|
|
300
|
+
items: list[dict[str, Any]] = []
|
|
301
|
+
for entry in data:
|
|
302
|
+
if not isinstance(entry, dict):
|
|
303
|
+
continue
|
|
304
|
+
normalized = self._normalize_rest(entry)
|
|
305
|
+
if normalized:
|
|
306
|
+
items.append(normalized)
|
|
307
|
+
|
|
308
|
+
self._clear()
|
|
309
|
+
if items:
|
|
310
|
+
self._insert(items)
|
|
311
|
+
|
|
312
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
313
|
+
results = msg.get("result") or []
|
|
314
|
+
for item in results:
|
|
315
|
+
data = item.get("data") if isinstance(item, dict) else None
|
|
316
|
+
normalized = self._normalize_ws(data or {})
|
|
317
|
+
if not normalized:
|
|
318
|
+
continue
|
|
319
|
+
key = {"instId": normalized["instId"], "posSide": normalized["posSide"]}
|
|
320
|
+
if normalized.get("pos") in {None, 0, "0"}:
|
|
321
|
+
self._delete([key])
|
|
322
|
+
continue
|
|
323
|
+
if self.get(key):
|
|
324
|
+
self._update([normalized])
|
|
325
|
+
else:
|
|
326
|
+
self._insert([normalized])
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class Orders(DataStore):
|
|
330
|
+
"""当前委托。"""
|
|
331
|
+
|
|
332
|
+
_KEYS = ["ordId"]
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@staticmethod
|
|
336
|
+
def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
337
|
+
ord_id = entry.get("ordId") or entry.get("ordID")
|
|
338
|
+
if not ord_id:
|
|
339
|
+
return None
|
|
340
|
+
normalized = dict(entry)
|
|
341
|
+
normalized["ordId"] = str(ord_id)
|
|
342
|
+
normalized = _ensure_identifiers(normalized)
|
|
343
|
+
if "instId" in normalized and normalized["instId"] is not None:
|
|
344
|
+
normalized["instId"] = str(normalized["instId"])
|
|
345
|
+
return normalized
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _normalize_ws(data: dict[str, Any]) -> dict[str, Any] | None:
|
|
349
|
+
if not isinstance(data, dict):
|
|
350
|
+
return None
|
|
351
|
+
mapping = {
|
|
352
|
+
"L": "clOrdId",
|
|
353
|
+
"I": "instId",
|
|
354
|
+
"OPT": "orderPriceType",
|
|
355
|
+
"D": "direction",
|
|
356
|
+
"o": "offsetFlag",
|
|
357
|
+
"P": "px",
|
|
358
|
+
"V": "sz",
|
|
359
|
+
"OT": "ordType",
|
|
360
|
+
"i": "isCrossMargin",
|
|
361
|
+
"OS": "ordId",
|
|
362
|
+
"l": "lever",
|
|
363
|
+
"Or": "state",
|
|
364
|
+
"v": "accFillSz",
|
|
365
|
+
"IT": "insertTime",
|
|
366
|
+
"U": "updateTime",
|
|
367
|
+
"T": "turnover",
|
|
368
|
+
"p": "posiDirection",
|
|
369
|
+
"t": "fillPx",
|
|
370
|
+
}
|
|
371
|
+
normalized: dict[str, Any] = {}
|
|
372
|
+
for key, value in data.items():
|
|
373
|
+
target = mapping.get(key)
|
|
374
|
+
if target:
|
|
375
|
+
normalized[target] = value
|
|
376
|
+
else:
|
|
377
|
+
normalized[key] = value
|
|
378
|
+
normalized = _ensure_identifiers(normalized)
|
|
379
|
+
if "ordId" not in normalized and "OS" in data:
|
|
380
|
+
normalized["ordId"] = str(data["OS"])
|
|
381
|
+
if "state" in normalized and isinstance(normalized["state"], (int, float)):
|
|
382
|
+
normalized["state"] = str(normalized["state"])
|
|
383
|
+
normalized = _ensure_identifiers(normalized)
|
|
384
|
+
if not normalized.get("ordId"):
|
|
385
|
+
return None
|
|
386
|
+
# state_tomap # 4 live 6 cancel 1 filled # todo
|
|
387
|
+
state_map = {
|
|
388
|
+
"0": "filled",
|
|
389
|
+
"1": "filled",
|
|
390
|
+
"2": "partially_filled",
|
|
391
|
+
"3": "partially_filled_canceled",
|
|
392
|
+
"4": "live",
|
|
393
|
+
"5": "nofill",
|
|
394
|
+
"6": "canceled",
|
|
395
|
+
"7": "canceled",
|
|
396
|
+
"filled": "filled",
|
|
397
|
+
"partially_filled": "partially_filled",
|
|
398
|
+
"partially_filled_canceled": "partially_filled_canceled",
|
|
399
|
+
"live": "live",
|
|
400
|
+
"nofill": "nofill",
|
|
401
|
+
"canceled": "canceled"
|
|
402
|
+
}
|
|
403
|
+
state = normalized.get("state")
|
|
404
|
+
if state in state_map:
|
|
405
|
+
normalized["state"] = state_map[state]
|
|
406
|
+
|
|
407
|
+
return normalized
|
|
408
|
+
|
|
409
|
+
def _on_response(self, msg: dict[str, Any]) -> None:
|
|
410
|
+
data = msg.get("data") or []
|
|
411
|
+
items: list[dict[str, Any]] = []
|
|
412
|
+
for entry in data:
|
|
413
|
+
if not isinstance(entry, dict):
|
|
414
|
+
continue
|
|
415
|
+
normalized = self._normalize_rest(entry)
|
|
416
|
+
if normalized:
|
|
417
|
+
items.append(normalized)
|
|
418
|
+
|
|
419
|
+
self._clear()
|
|
420
|
+
if items:
|
|
421
|
+
self._insert(items)
|
|
422
|
+
|
|
423
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
424
|
+
# 4 live 6 cancel 1 filled
|
|
425
|
+
results = msg.get("result") or []
|
|
426
|
+
deletes: list[dict[str, Any]] = []
|
|
427
|
+
updates: list[dict[str, Any]] = []
|
|
428
|
+
inserts: list[dict[str, Any]] = []
|
|
429
|
+
for item in results:
|
|
430
|
+
data = item.get("data") if isinstance(item, dict) else None
|
|
431
|
+
normalized = self._normalize_ws(data or {})
|
|
432
|
+
if not normalized:
|
|
433
|
+
continue
|
|
434
|
+
key = {"ordId": normalized["ordId"]}
|
|
435
|
+
if self.get(key):
|
|
436
|
+
updates.append(normalized)
|
|
437
|
+
else:
|
|
438
|
+
inserts.append(normalized)
|
|
439
|
+
state = str(normalized.get("state") or "").lower()
|
|
440
|
+
if state in {"filled", "canceled", "nofill", "partially_filled_canceled"}:
|
|
441
|
+
deletes.append(normalized)
|
|
442
|
+
if inserts:
|
|
443
|
+
self._insert(inserts)
|
|
444
|
+
if updates:
|
|
445
|
+
self._update(updates)
|
|
446
|
+
if deletes:
|
|
447
|
+
self._delete(deletes)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
class Trades(DataStore):
|
|
451
|
+
"""成交明细。"""
|
|
452
|
+
|
|
453
|
+
_KEYS = ["tradeId"]
|
|
454
|
+
|
|
455
|
+
@staticmethod
|
|
456
|
+
def _normalize_rest(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
457
|
+
trade_id = entry.get("tradeId") or entry.get("tradeID")
|
|
458
|
+
if not trade_id:
|
|
459
|
+
return None
|
|
460
|
+
normalized = _ensure_identifiers(dict(entry))
|
|
461
|
+
normalized["tradeId"] = str(trade_id)
|
|
462
|
+
return normalized
|
|
463
|
+
|
|
464
|
+
@staticmethod
|
|
465
|
+
def _normalize_ws(data: dict[str, Any]) -> dict[str, Any] | None:
|
|
466
|
+
if not isinstance(data, dict):
|
|
467
|
+
return None
|
|
468
|
+
mapping = {
|
|
469
|
+
"TI": "tradeId",
|
|
470
|
+
"D": "direction",
|
|
471
|
+
"OS": "ordId",
|
|
472
|
+
"M": "memberId",
|
|
473
|
+
"A": "accountId",
|
|
474
|
+
"I": "instId",
|
|
475
|
+
"o": "offsetFlag",
|
|
476
|
+
"P": "px",
|
|
477
|
+
"V": "sz",
|
|
478
|
+
"TT": "tradeTime",
|
|
479
|
+
"IT": "insertTime",
|
|
480
|
+
"T": "turnover",
|
|
481
|
+
"F": "fee",
|
|
482
|
+
"f": "feeCcy",
|
|
483
|
+
"CC": "clearCurrency",
|
|
484
|
+
"m": "matchRole",
|
|
485
|
+
"l": "lever",
|
|
486
|
+
"CP": "closeProfit",
|
|
487
|
+
}
|
|
488
|
+
normalized: dict[str, Any] = {}
|
|
489
|
+
for key, value in data.items():
|
|
490
|
+
target = mapping.get(key)
|
|
491
|
+
if target:
|
|
492
|
+
normalized[target] = value
|
|
493
|
+
else:
|
|
494
|
+
normalized[key] = value
|
|
495
|
+
normalized = _ensure_identifiers(normalized)
|
|
496
|
+
if "tradeId" not in normalized and "TI" in data:
|
|
497
|
+
normalized["tradeId"] = str(data["TI"])
|
|
498
|
+
if not normalized.get("tradeId"):
|
|
499
|
+
return None
|
|
500
|
+
return normalized
|
|
501
|
+
|
|
502
|
+
def _on_response(self, msg: dict[str, Any]) -> None:
|
|
503
|
+
data = msg.get("data") or []
|
|
504
|
+
items: list[dict[str, Any]] = []
|
|
505
|
+
for entry in data:
|
|
506
|
+
if not isinstance(entry, dict):
|
|
507
|
+
continue
|
|
508
|
+
normalized = self._normalize_rest(entry)
|
|
509
|
+
if normalized:
|
|
510
|
+
items.append(normalized)
|
|
511
|
+
|
|
512
|
+
if not items:
|
|
513
|
+
return
|
|
514
|
+
keys = [{"tradeId": item["tradeId"]} for item in items]
|
|
515
|
+
self._delete(keys)
|
|
516
|
+
self._insert(items)
|
|
517
|
+
|
|
518
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
519
|
+
results = msg.get("result") or []
|
|
520
|
+
items: list[dict[str, Any]] = []
|
|
521
|
+
for item in results:
|
|
522
|
+
data = item.get("data") if isinstance(item, dict) else None
|
|
523
|
+
normalized = self._normalize_ws(data or {})
|
|
524
|
+
if normalized:
|
|
525
|
+
items.append(normalized)
|
|
526
|
+
if not items:
|
|
527
|
+
return
|
|
528
|
+
keys = [{"tradeId": item["tradeId"]} for item in items]
|
|
529
|
+
self._delete(keys)
|
|
530
|
+
self._insert(items)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
class DeepCoinDataStore(DataStoreCollection):
|
|
534
|
+
"""DeepCoin 合约数据存储集合"""
|
|
535
|
+
|
|
536
|
+
def _init(self) -> None:
|
|
537
|
+
self._create("detail", datastore_class=Detail)
|
|
538
|
+
self._create("ticker", datastore_class=Ticker)
|
|
539
|
+
self._create("book", datastore_class=Book)
|
|
540
|
+
self._create("orders", datastore_class=Orders)
|
|
541
|
+
self._create("position", datastore_class=Position)
|
|
542
|
+
self._create("balance", datastore_class=Balance)
|
|
543
|
+
self._create("trades", datastore_class=Trades)
|
|
544
|
+
|
|
545
|
+
def _on_message(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
546
|
+
chan = msg.get("a")
|
|
547
|
+
if chan == 'PO':
|
|
548
|
+
self.ticker._on_message(msg)
|
|
549
|
+
self.book._on_message(msg)
|
|
550
|
+
return
|
|
551
|
+
|
|
552
|
+
action = msg.get("action")
|
|
553
|
+
if action == "PushOrder":
|
|
554
|
+
self.orders._on_message(msg)
|
|
555
|
+
elif action == "PushAccount":
|
|
556
|
+
self.balance._on_message(msg)
|
|
557
|
+
elif action == "PushPosition":
|
|
558
|
+
self.position._on_message(msg)
|
|
559
|
+
elif action == "PushTrade":
|
|
560
|
+
self.trades._on_message(msg)
|
|
561
|
+
|
|
562
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
563
|
+
self._on_message(msg, ws)
|
|
564
|
+
|
|
565
|
+
async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
|
|
566
|
+
for fut in asyncio.as_completed(aws):
|
|
567
|
+
res = await fut
|
|
568
|
+
data = await res.json()
|
|
569
|
+
path = res.url.path
|
|
570
|
+
if path.endswith("/market/instruments"):
|
|
571
|
+
self.detail._clear()
|
|
572
|
+
self.detail._on_response(data)
|
|
573
|
+
elif path.endswith("/market/tickers"):
|
|
574
|
+
self.ticker._on_response(data)
|
|
575
|
+
elif path.endswith("/trade/v2/orders-pending"):
|
|
576
|
+
self.orders._on_response(data)
|
|
577
|
+
elif path.endswith("/account/positions"):
|
|
578
|
+
self.position._on_response(data)
|
|
579
|
+
elif path.endswith("/account/balances"):
|
|
580
|
+
self.balance._on_response(data)
|
|
581
|
+
elif path.endswith("/trade/fills"):
|
|
582
|
+
self.trades._on_response(data)
|
|
583
|
+
elif path.endswith("/trade/orders-history"):
|
|
584
|
+
self.orders._on_response(data)
|
|
585
|
+
|
|
586
|
+
@property
|
|
587
|
+
def ticker(self) -> Ticker:
|
|
588
|
+
"""
|
|
589
|
+
_key: s
|
|
590
|
+
.. code :: json
|
|
591
|
+
|
|
592
|
+
[{
|
|
593
|
+
"s": "BTCUSDT",
|
|
594
|
+
"I": "BTCUSDT",
|
|
595
|
+
"U": 1757642301089,
|
|
596
|
+
"PF": 1756690200,
|
|
597
|
+
"E": 0.0005251816,
|
|
598
|
+
"O": 114206.7,
|
|
599
|
+
"H": 116346,
|
|
600
|
+
"L": 114132.8,
|
|
601
|
+
"V": 7688046,
|
|
602
|
+
"T": 885654450.392686,
|
|
603
|
+
"N": 115482.9,
|
|
604
|
+
"M": 115473.7,
|
|
605
|
+
"D": 115455.77,
|
|
606
|
+
"V2": 19978848,
|
|
607
|
+
"T2": 2288286517.724497,
|
|
608
|
+
"F": 57727.9,
|
|
609
|
+
"C": 173183.6,
|
|
610
|
+
"BP1": 115482.8,
|
|
611
|
+
"AP1": 115482.9
|
|
612
|
+
}]
|
|
613
|
+
"""
|
|
614
|
+
return self._get("ticker")
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def book(self) -> Book:
|
|
618
|
+
"""
|
|
619
|
+
_key: s, S
|
|
620
|
+
.. code :: json
|
|
621
|
+
|
|
622
|
+
[
|
|
623
|
+
{
|
|
624
|
+
"s": "BTCUSDT",
|
|
625
|
+
"S": "b",
|
|
626
|
+
"p": 115482.8,
|
|
627
|
+
"q": 0
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
"s": "BTCUSDT",
|
|
631
|
+
"S": "a",
|
|
632
|
+
"p": 115482.9,
|
|
633
|
+
"q": 0
|
|
634
|
+
}
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
"""
|
|
638
|
+
return self._get("book")
|
|
639
|
+
|
|
640
|
+
@property
|
|
641
|
+
def detail(self) -> Detail:
|
|
642
|
+
"""
|
|
643
|
+
_key: s
|
|
644
|
+
.. code :: json
|
|
645
|
+
|
|
646
|
+
[
|
|
647
|
+
{
|
|
648
|
+
"s": "BTCUSDT",
|
|
649
|
+
"instType": "SWAP",
|
|
650
|
+
"instId": "BTC-USDT-SWAP",
|
|
651
|
+
"uly": "",
|
|
652
|
+
"baseCcy": "BTC",
|
|
653
|
+
"quoteCcy": "USDT",
|
|
654
|
+
"ctVal": "0.001",
|
|
655
|
+
"ctValCcy": "",
|
|
656
|
+
"listTime": "0",
|
|
657
|
+
"lever": "125",
|
|
658
|
+
"tickSz": "0.1",
|
|
659
|
+
"lotSz": "1",
|
|
660
|
+
"minSz": "1",
|
|
661
|
+
"ctType": "",
|
|
662
|
+
"alias": "",
|
|
663
|
+
"state": "live",
|
|
664
|
+
"maxLmtSz": "200000",
|
|
665
|
+
"maxMktSz": "200000",
|
|
666
|
+
"tick_size": "0.1",
|
|
667
|
+
"step_size": "1"
|
|
668
|
+
}
|
|
669
|
+
]
|
|
670
|
+
"""
|
|
671
|
+
return self._get("detail")
|
|
672
|
+
|
|
673
|
+
@property
|
|
674
|
+
def orders(self) -> Orders:
|
|
675
|
+
"""
|
|
676
|
+
当前委托订单数据。
|
|
677
|
+
|
|
678
|
+
该数据结构用于记录当前账户下所有活跃的委托订单(如限价单、市价单等),
|
|
679
|
+
包含订单的唯一标识、交易对、状态、价格、数量等关键信息。生命周期上,
|
|
680
|
+
订单在下单、成交、撤单等过程中会不断更新,配合 `trades` 可追踪完整的订单执行历程。
|
|
681
|
+
|
|
682
|
+
_key: ordId
|
|
683
|
+
.. code :: json
|
|
684
|
+
|
|
685
|
+
[
|
|
686
|
+
{
|
|
687
|
+
"ordId": "1234567890",
|
|
688
|
+
"instId": "BTC-USDT-SWAP",
|
|
689
|
+
"clOrdId": "myorder001",
|
|
690
|
+
"px": "115000",
|
|
691
|
+
"sz": "0.01",
|
|
692
|
+
"ordType": "limit",
|
|
693
|
+
"side": "buy",
|
|
694
|
+
"state": "live",
|
|
695
|
+
"accFillSz": "0",
|
|
696
|
+
"insertTime": 1711111111111,
|
|
697
|
+
"updateTime": 1711111112222
|
|
698
|
+
}
|
|
699
|
+
]
|
|
700
|
+
主要字段:
|
|
701
|
+
- ordId: 订单唯一id
|
|
702
|
+
- instId: 交易对
|
|
703
|
+
- px: 委托价格
|
|
704
|
+
- sz: 委托数量
|
|
705
|
+
- ordType: 订单类型(如限价、市价)
|
|
706
|
+
- state: 当前订单状态(如live/partially_filled/filled/canceled等)
|
|
707
|
+
- accFillSz: 已成交数量
|
|
708
|
+
- insertTime: 下单时间
|
|
709
|
+
- updateTime: 更新时间
|
|
710
|
+
"""
|
|
711
|
+
return self._get("orders")
|
|
712
|
+
|
|
713
|
+
@property
|
|
714
|
+
def position(self) -> Position:
|
|
715
|
+
"""
|
|
716
|
+
当前持仓数据。
|
|
717
|
+
|
|
718
|
+
记录账户在各交易对上的持仓情况,包括方向、多空、持仓量、均价、杠杆等。
|
|
719
|
+
持仓数据在开仓、平仓、爆仓等事件发生时实时更新,是风险监控与盈亏计算的基础。
|
|
720
|
+
|
|
721
|
+
_key: instId, posSide
|
|
722
|
+
.. code :: json
|
|
723
|
+
|
|
724
|
+
[
|
|
725
|
+
{
|
|
726
|
+
"instType": "SWAP",
|
|
727
|
+
"mgnMode": "cross",
|
|
728
|
+
"instId": "DOT-USDT-SWAP",
|
|
729
|
+
"posId": "1001113501647163",
|
|
730
|
+
"posSide": "long",
|
|
731
|
+
"pos": "5",
|
|
732
|
+
"avgPx": "2.624",
|
|
733
|
+
"lever": "20",
|
|
734
|
+
"liqPx": "0.001",
|
|
735
|
+
"useMargin": "0.0656",
|
|
736
|
+
"unrealizedProfit": "0.001000000000000112",
|
|
737
|
+
"mrgPosition": "merge",
|
|
738
|
+
"ccy": "USDT",
|
|
739
|
+
"uTime": "1762350896000",
|
|
740
|
+
"cTime": "1762350896000"
|
|
741
|
+
}
|
|
742
|
+
]
|
|
743
|
+
"""
|
|
744
|
+
return self._get("position")
|
|
745
|
+
|
|
746
|
+
@property
|
|
747
|
+
def balance(self) -> Balance:
|
|
748
|
+
"""
|
|
749
|
+
账户余额数据。
|
|
750
|
+
|
|
751
|
+
反映账户在各币种上的余额、可用余额、冻结余额等资金情况,是资金划转与风险控制的重要依据。
|
|
752
|
+
余额数据在充值、提现、成交、资金划转等环节实时变动。
|
|
753
|
+
|
|
754
|
+
_key: ccy
|
|
755
|
+
.. code :: json
|
|
756
|
+
|
|
757
|
+
[
|
|
758
|
+
{
|
|
759
|
+
"ccy": "USDT",
|
|
760
|
+
"bal": "1000.0",
|
|
761
|
+
"availBal": "800.0",
|
|
762
|
+
"frozenBal": "200.0",
|
|
763
|
+
"withdrawable": "750.0"
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
主要字段:
|
|
767
|
+
- ccy: 币种
|
|
768
|
+
- bal: 总余额
|
|
769
|
+
- availBal: 可用余额
|
|
770
|
+
- frozenBal: 冻结余额
|
|
771
|
+
- withdrawable: 可提余额
|
|
772
|
+
"""
|
|
773
|
+
return self._get("balance")
|
|
774
|
+
|
|
775
|
+
@property
|
|
776
|
+
def trades(self) -> Trades:
|
|
777
|
+
"""
|
|
778
|
+
成交明细数据。
|
|
779
|
+
|
|
780
|
+
记录账户所有历史成交(包括主动成交与被动成交),用于统计订单执行情况、盈亏分析和对账。
|
|
781
|
+
每条成交数据包含唯一成交id、订单id、成交方向、成交数量、成交价格、手续费等。
|
|
782
|
+
|
|
783
|
+
_key: tradeId
|
|
784
|
+
.. code :: json
|
|
785
|
+
|
|
786
|
+
[
|
|
787
|
+
{
|
|
788
|
+
"tradeId": "9876543210",
|
|
789
|
+
"ordId": "1234567890",
|
|
790
|
+
"instId": "BTCUSDT",
|
|
791
|
+
"direction": "buy",
|
|
792
|
+
"sz": "0.01",
|
|
793
|
+
"px": "115100",
|
|
794
|
+
"fee": "-0.02",
|
|
795
|
+
"feeCcy": "USDT",
|
|
796
|
+
"tradeTime": 1711111114444
|
|
797
|
+
}
|
|
798
|
+
]
|
|
799
|
+
主要字段:
|
|
800
|
+
- tradeId: 成交唯一id
|
|
801
|
+
- ordId: 所属订单id
|
|
802
|
+
- instId: 交易对
|
|
803
|
+
- direction: 买卖方向
|
|
804
|
+
- sz: 成交数量
|
|
805
|
+
- px: 成交价格
|
|
806
|
+
- fee: 手续费
|
|
807
|
+
- tradeTime: 成交时间
|
|
808
|
+
"""
|
|
809
|
+
return self._get("trades")
|