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,651 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
from decimal import Decimal, ROUND_CEILING, ROUND_DOWN, ROUND_HALF_UP
|
|
8
|
+
from typing import Any, Sequence, Literal
|
|
9
|
+
|
|
10
|
+
import pybotters
|
|
11
|
+
|
|
12
|
+
from .models.deepcoin import DeepCoinDataStore
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DeepCoin:
|
|
18
|
+
"""DeepCoin 合约客户端(REST + WebSocket)。"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
client: pybotters.Client,
|
|
23
|
+
*,
|
|
24
|
+
rest_api: str | None = None,
|
|
25
|
+
ws_public: str | None = None,
|
|
26
|
+
ws_private: str | None = None,
|
|
27
|
+
inst_type: str = "SWAP",
|
|
28
|
+
) -> None:
|
|
29
|
+
self.client = client
|
|
30
|
+
self.store = DeepCoinDataStore()
|
|
31
|
+
|
|
32
|
+
self.rest_api = rest_api or "https://api.deepcoin.com"
|
|
33
|
+
self.ws_public = (
|
|
34
|
+
ws_public
|
|
35
|
+
or "wss://stream.deepcoin.com/streamlet/trade/public/swap?platform=api"
|
|
36
|
+
)
|
|
37
|
+
self.ws_private = ws_private or "wss://stream.deepcoin.com/v1/private"
|
|
38
|
+
self.inst_type = inst_type
|
|
39
|
+
|
|
40
|
+
self._ws_public: pybotters.ws.WebSocketApp | None = None
|
|
41
|
+
self._ws_public_ready = asyncio.Event()
|
|
42
|
+
self._ws_private: pybotters.ws.WebSocketApp | None = None
|
|
43
|
+
self._ws_private_ready = asyncio.Event()
|
|
44
|
+
self._listen_key: str | None = None
|
|
45
|
+
self._listen_key_expire_at: float = 0.0
|
|
46
|
+
self._listen_key_task: asyncio.Task | None = None
|
|
47
|
+
self._listen_key_lock = asyncio.Lock()
|
|
48
|
+
|
|
49
|
+
async def __aenter__(self) -> "DeepCoin":
|
|
50
|
+
await self.update("detail")
|
|
51
|
+
return self
|
|
52
|
+
|
|
53
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
|
|
54
|
+
await self.aclose()
|
|
55
|
+
|
|
56
|
+
async def aclose(self) -> None:
|
|
57
|
+
if self._ws_public is not None:
|
|
58
|
+
await self._ws_public.current_ws.close()
|
|
59
|
+
self._ws_public = None
|
|
60
|
+
self._ws_public_ready.clear()
|
|
61
|
+
if self._ws_private is not None:
|
|
62
|
+
await self._ws_private.current_ws.close()
|
|
63
|
+
self._ws_private = None
|
|
64
|
+
self._ws_private_ready.clear()
|
|
65
|
+
if self._listen_key_task is not None:
|
|
66
|
+
self._listen_key_task.cancel()
|
|
67
|
+
with suppress(Exception):
|
|
68
|
+
await self._listen_key_task
|
|
69
|
+
self._listen_key_task = None
|
|
70
|
+
self._listen_key = None
|
|
71
|
+
self._listen_key_expire_at = 0.0
|
|
72
|
+
|
|
73
|
+
async def update(
|
|
74
|
+
self,
|
|
75
|
+
update_type: Literal[
|
|
76
|
+
"all", "detail", "ticker", "orders", "positions", "balances", "trades", "orders-history"
|
|
77
|
+
] = "all",
|
|
78
|
+
*,
|
|
79
|
+
inst_type: str | None = None,
|
|
80
|
+
inst_id: str | None = None,
|
|
81
|
+
symbol: str | None = None,
|
|
82
|
+
page: int = 1,
|
|
83
|
+
limit: int | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""刷新本地缓存(detail / ticker / 私有数据)。"""
|
|
86
|
+
|
|
87
|
+
inst = inst_type or self.inst_type
|
|
88
|
+
requests: list[Any] = []
|
|
89
|
+
|
|
90
|
+
include_detail = update_type in {"detail", "all"}
|
|
91
|
+
include_ticker = update_type in {"ticker", "all"}
|
|
92
|
+
include_orders = update_type in {"orders"} or (update_type == "all" and inst_id)
|
|
93
|
+
include_history_orders = update_type in {"orders-history"}
|
|
94
|
+
include_positions = update_type in {"position", "positions", "all"}
|
|
95
|
+
include_balances = update_type in {"balance", "balances", "account", "all"}
|
|
96
|
+
include_trades = update_type in {"trade", "trades"} or (
|
|
97
|
+
update_type == "all" and inst_id
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if include_detail:
|
|
101
|
+
params = {"instType": inst}
|
|
102
|
+
requests.append(
|
|
103
|
+
self.client.get(
|
|
104
|
+
f"{self.rest_api}/deepcoin/market/instruments", params=params
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if include_ticker:
|
|
109
|
+
params = {"instType": inst}
|
|
110
|
+
requests.append(
|
|
111
|
+
self.client.get(
|
|
112
|
+
f"{self.rest_api}/deepcoin/market/tickers", params=params
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if include_history_orders:
|
|
117
|
+
params = {"instType": inst}
|
|
118
|
+
if limit:
|
|
119
|
+
params["limit"] = limit
|
|
120
|
+
|
|
121
|
+
requests.append(
|
|
122
|
+
self.client.get(
|
|
123
|
+
f"{self.rest_api}/deepcoin/trade/orders-history",
|
|
124
|
+
params=params,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if include_orders:
|
|
129
|
+
if not inst_id:
|
|
130
|
+
raise ValueError("inst_id is required when updating orders")
|
|
131
|
+
params = {"instId": inst_id, "index": page}
|
|
132
|
+
if limit is not None:
|
|
133
|
+
params["limit"] = limit
|
|
134
|
+
requests.append(
|
|
135
|
+
self.client.get(
|
|
136
|
+
f"{self.rest_api}/deepcoin/trade/v2/orders-pending",
|
|
137
|
+
params=params,
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if include_positions:
|
|
142
|
+
params = {"instType": inst}
|
|
143
|
+
|
|
144
|
+
requests.append(
|
|
145
|
+
self.client.get(
|
|
146
|
+
f"{self.rest_api}/deepcoin/account/positions",
|
|
147
|
+
params=params,
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if include_balances:
|
|
152
|
+
params = {"instType": inst}
|
|
153
|
+
requests.append(
|
|
154
|
+
self.client.get(
|
|
155
|
+
f"{self.rest_api}/deepcoin/account/balances",
|
|
156
|
+
params=params,
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if include_trades:
|
|
161
|
+
if not inst_id:
|
|
162
|
+
raise ValueError("inst_id is required when updating trades")
|
|
163
|
+
params = {"instId": inst_id}
|
|
164
|
+
if limit is not None:
|
|
165
|
+
params["limit"] = limit
|
|
166
|
+
requests.append(
|
|
167
|
+
self.client.get(
|
|
168
|
+
f"{self.rest_api}/deepcoin/trade/fills",
|
|
169
|
+
params=params,
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not requests:
|
|
174
|
+
raise ValueError(f"Unsupported update_type: {update_type}")
|
|
175
|
+
|
|
176
|
+
await self.store.initialize(*requests)
|
|
177
|
+
|
|
178
|
+
async def sub_ticker(
|
|
179
|
+
self,
|
|
180
|
+
symbols: Sequence[str] | str,
|
|
181
|
+
*,
|
|
182
|
+
resume_no: int = -1,
|
|
183
|
+
local_no_start: int = 1,
|
|
184
|
+
action: str = "1",
|
|
185
|
+
) -> pybotters.ws.WebSocketApp:
|
|
186
|
+
"""订阅 PO 频道(顶深度行情)。"""
|
|
187
|
+
|
|
188
|
+
if isinstance(symbols, str):
|
|
189
|
+
symbol_list = [symbols]
|
|
190
|
+
else:
|
|
191
|
+
symbol_list = list(symbols)
|
|
192
|
+
|
|
193
|
+
if not symbol_list:
|
|
194
|
+
raise ValueError("symbols must not be empty")
|
|
195
|
+
|
|
196
|
+
payload: list[dict[str, Any]] = []
|
|
197
|
+
for idx, symbol in enumerate(symbol_list):
|
|
198
|
+
payload.append(
|
|
199
|
+
{
|
|
200
|
+
"SendTopicAction": {
|
|
201
|
+
"Action": action,
|
|
202
|
+
"FilterValue": f"DeepCoin_{symbol}",
|
|
203
|
+
"LocalNo": local_no_start + idx,
|
|
204
|
+
"ResumeNo": resume_no,
|
|
205
|
+
"TopicID": "7",
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
ws_app = self.client.ws_connect(
|
|
211
|
+
self.ws_public,
|
|
212
|
+
send_json=payload if len(payload) > 1 else payload[0],
|
|
213
|
+
hdlr_json=self.store.onmessage,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
await ws_app._event.wait()
|
|
217
|
+
self._ws_public = ws_app
|
|
218
|
+
self._ws_public_ready.set()
|
|
219
|
+
return ws_app
|
|
220
|
+
|
|
221
|
+
async def sub_orderbook(
|
|
222
|
+
self,
|
|
223
|
+
symbols: Sequence[str] | str,
|
|
224
|
+
**kwargs: Any,
|
|
225
|
+
) -> pybotters.ws.WebSocketApp:
|
|
226
|
+
"""订阅顶档深度(PO 频道)的别名。"""
|
|
227
|
+
|
|
228
|
+
return await self.sub_ticker(symbols, **kwargs)
|
|
229
|
+
|
|
230
|
+
async def _acquire_listen_key(self) -> str:
|
|
231
|
+
res = await self.client.get(f"{self.rest_api}/deepcoin/listenkey/acquire")
|
|
232
|
+
payload = await res.json()
|
|
233
|
+
data = payload.get("data") or {}
|
|
234
|
+
listen_key = data.get("listenkey")
|
|
235
|
+
if not listen_key:
|
|
236
|
+
raise RuntimeError(f"Failed to acquire DeepCoin listenKey: {payload}")
|
|
237
|
+
expire_time = data.get("expire_time")
|
|
238
|
+
self._listen_key = listen_key
|
|
239
|
+
self._listen_key_expire_at = (
|
|
240
|
+
float(expire_time) if expire_time else time.time() + 3600
|
|
241
|
+
)
|
|
242
|
+
return listen_key
|
|
243
|
+
|
|
244
|
+
async def _extend_listen_key(self) -> None:
|
|
245
|
+
if not self._listen_key:
|
|
246
|
+
raise RuntimeError("listenKey not initialized")
|
|
247
|
+
params = {"listenkey": self._listen_key}
|
|
248
|
+
res = await self.client.get(
|
|
249
|
+
f"{self.rest_api}/deepcoin/listenkey/extend", params=params
|
|
250
|
+
)
|
|
251
|
+
payload = await res.json()
|
|
252
|
+
data = payload.get("data") or {}
|
|
253
|
+
expire_time = data.get("expire_time")
|
|
254
|
+
if expire_time:
|
|
255
|
+
self._listen_key_expire_at = float(expire_time)
|
|
256
|
+
|
|
257
|
+
async def _ensure_listen_key(self) -> str:
|
|
258
|
+
async with self._listen_key_lock:
|
|
259
|
+
now = time.time()
|
|
260
|
+
if self._listen_key and now < self._listen_key_expire_at - 300:
|
|
261
|
+
return self._listen_key
|
|
262
|
+
if self._listen_key and now < self._listen_key_expire_at - 60:
|
|
263
|
+
await self._extend_listen_key()
|
|
264
|
+
return self._listen_key
|
|
265
|
+
return await self._acquire_listen_key()
|
|
266
|
+
|
|
267
|
+
async def _keep_listen_key_alive(self) -> None:
|
|
268
|
+
try:
|
|
269
|
+
while True:
|
|
270
|
+
await asyncio.sleep(1800)
|
|
271
|
+
if self._listen_key is None:
|
|
272
|
+
continue
|
|
273
|
+
try:
|
|
274
|
+
await self._extend_listen_key()
|
|
275
|
+
except Exception:
|
|
276
|
+
logger.exception("DeepCoin listenKey keepalive failed")
|
|
277
|
+
except asyncio.CancelledError: # pragma: no cover - task control flow
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
async def sub_private(self) -> pybotters.ws.WebSocketApp:
|
|
281
|
+
"""订阅私有频道,推送订单/资金/持仓/成交。"""
|
|
282
|
+
|
|
283
|
+
if self._ws_private is not None and not self._ws_private.closed:
|
|
284
|
+
return self._ws_private
|
|
285
|
+
|
|
286
|
+
listen_key = await self._ensure_listen_key()
|
|
287
|
+
url = f"{self.ws_private}?listenKey={listen_key}"
|
|
288
|
+
ws_app = self.client.ws_connect(
|
|
289
|
+
url,
|
|
290
|
+
hdlr_json=self.store.onmessage,
|
|
291
|
+
)
|
|
292
|
+
await ws_app._event.wait()
|
|
293
|
+
self._ws_private = ws_app
|
|
294
|
+
self._ws_private_ready.set()
|
|
295
|
+
if self._listen_key_task is None or self._listen_key_task.done():
|
|
296
|
+
self._listen_key_task = asyncio.create_task(self._keep_listen_key_alive())
|
|
297
|
+
return ws_app
|
|
298
|
+
|
|
299
|
+
# ------------------------------------------------------------------
|
|
300
|
+
# Helpers
|
|
301
|
+
|
|
302
|
+
async def _ensure_detail_cache(self) -> None:
|
|
303
|
+
if not self.store.detail.find():
|
|
304
|
+
await self.update("detail")
|
|
305
|
+
|
|
306
|
+
async def _resolve_instrument(
|
|
307
|
+
self,
|
|
308
|
+
inst_id: str | None = None,
|
|
309
|
+
symbol: str | None = None,
|
|
310
|
+
) -> tuple[str, dict[str, Any]]:
|
|
311
|
+
await self._ensure_detail_cache()
|
|
312
|
+
|
|
313
|
+
entries = list(self.store.detail.find())
|
|
314
|
+
|
|
315
|
+
def _enrich(detail: dict[str, Any]) -> tuple[str, dict[str, Any]] | None:
|
|
316
|
+
if not detail:
|
|
317
|
+
return None
|
|
318
|
+
inst = detail.get("instId")
|
|
319
|
+
if not inst:
|
|
320
|
+
base = detail.get("baseCcy") or detail.get("base")
|
|
321
|
+
quote = detail.get("quoteCcy") or detail.get("quote")
|
|
322
|
+
inst_type = detail.get("instType")
|
|
323
|
+
if base and quote:
|
|
324
|
+
inst = f"{base}-{quote}"
|
|
325
|
+
if inst_type:
|
|
326
|
+
inst = f"{inst}-{str(inst_type).upper()}"
|
|
327
|
+
if inst:
|
|
328
|
+
detail = dict(detail)
|
|
329
|
+
detail["instId"] = inst
|
|
330
|
+
return inst, detail
|
|
331
|
+
return None
|
|
332
|
+
|
|
333
|
+
if inst_id:
|
|
334
|
+
search_keys = {
|
|
335
|
+
inst_id,
|
|
336
|
+
inst_id.upper(),
|
|
337
|
+
inst_id.replace("-", ""),
|
|
338
|
+
inst_id.replace("-", "").upper(),
|
|
339
|
+
}
|
|
340
|
+
for detail in entries:
|
|
341
|
+
inst = str(detail.get("instId", ""))
|
|
342
|
+
if inst and inst in search_keys:
|
|
343
|
+
enriched = _enrich(detail)
|
|
344
|
+
if enriched:
|
|
345
|
+
return enriched
|
|
346
|
+
s_val = str(detail.get("s", ""))
|
|
347
|
+
if s_val and s_val in search_keys:
|
|
348
|
+
enriched = _enrich(detail)
|
|
349
|
+
if enriched:
|
|
350
|
+
return enriched
|
|
351
|
+
|
|
352
|
+
if symbol:
|
|
353
|
+
normalized_symbol = symbol.replace("-", "").upper()
|
|
354
|
+
for detail in entries:
|
|
355
|
+
s_val = str(detail.get("s", "")).upper()
|
|
356
|
+
inst = str(detail.get("instId", ""))
|
|
357
|
+
inst_compact = inst.replace("-", "").upper()
|
|
358
|
+
if normalized_symbol in {s_val, inst_compact}:
|
|
359
|
+
enriched = _enrich(detail)
|
|
360
|
+
if enriched:
|
|
361
|
+
return enriched
|
|
362
|
+
|
|
363
|
+
if normalized_symbol.endswith("USDT") and self.inst_type.upper() == "SWAP":
|
|
364
|
+
base = normalized_symbol[:-4]
|
|
365
|
+
guess = f"{base}-USDT-SWAP"
|
|
366
|
+
return await self._resolve_instrument(inst_id=guess)
|
|
367
|
+
|
|
368
|
+
# fallback: refresh detail and try again
|
|
369
|
+
await self.update("detail")
|
|
370
|
+
if inst_id or symbol:
|
|
371
|
+
return await self._resolve_instrument(inst_id=inst_id, symbol=symbol)
|
|
372
|
+
|
|
373
|
+
raise ValueError(
|
|
374
|
+
"Unable to resolve instrument; please provide inst_id or symbol"
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
@staticmethod
|
|
378
|
+
def _to_decimal(value: float | str | Decimal) -> Decimal:
|
|
379
|
+
if isinstance(value, Decimal):
|
|
380
|
+
return value
|
|
381
|
+
return Decimal(str(value))
|
|
382
|
+
|
|
383
|
+
@staticmethod
|
|
384
|
+
def _quantize(
|
|
385
|
+
value: Decimal, step: str | float | Decimal | None, rounding
|
|
386
|
+
) -> Decimal:
|
|
387
|
+
if step is None:
|
|
388
|
+
return value
|
|
389
|
+
step_dec = Decimal(str(step))
|
|
390
|
+
if step_dec == 0:
|
|
391
|
+
return value
|
|
392
|
+
return (value / step_dec).to_integral_value(rounding=rounding) * step_dec
|
|
393
|
+
|
|
394
|
+
@staticmethod
|
|
395
|
+
def _ceil_to_step(value: Decimal, step: str | float | Decimal | None) -> Decimal:
|
|
396
|
+
if step is None:
|
|
397
|
+
return value
|
|
398
|
+
step_dec = Decimal(str(step))
|
|
399
|
+
if step_dec == 0:
|
|
400
|
+
return value
|
|
401
|
+
return (value / step_dec).to_integral_value(rounding=ROUND_CEILING) * step_dec
|
|
402
|
+
|
|
403
|
+
@staticmethod
|
|
404
|
+
def _decimal_to_str(value: Decimal) -> str:
|
|
405
|
+
s = format(value, "f")
|
|
406
|
+
if "." in s:
|
|
407
|
+
s = s.rstrip("0").rstrip(".")
|
|
408
|
+
return s or "0"
|
|
409
|
+
|
|
410
|
+
def inst_id_to_symbol(self, inst_id: str) -> str:
|
|
411
|
+
"""将 DeepCoin 的 inst_id 转换为标准 symbol 格式。"""
|
|
412
|
+
parts = inst_id.split("-")
|
|
413
|
+
if len(parts) >= 2:
|
|
414
|
+
base = parts[0]
|
|
415
|
+
quote = parts[1]
|
|
416
|
+
return f"{base}{quote}"
|
|
417
|
+
return inst_id
|
|
418
|
+
|
|
419
|
+
def symbol_to_inst_id(self, symbol: str) -> str:
|
|
420
|
+
"""将标准 symbol 格式转换为 DeepCoin 的 inst_id 格式。"""
|
|
421
|
+
if symbol.endswith("USDT"):
|
|
422
|
+
base = symbol[:-4]
|
|
423
|
+
quote = "USDT"
|
|
424
|
+
elif symbol.endswith("USD"):
|
|
425
|
+
base = symbol[:-3]
|
|
426
|
+
quote = "USD"
|
|
427
|
+
else:
|
|
428
|
+
raise ValueError(f"Unsupported symbol format: {symbol}")
|
|
429
|
+
return f"{base}-{quote}-SWAP"
|
|
430
|
+
|
|
431
|
+
# ------------------------------------------------------------------
|
|
432
|
+
# Trading APIs
|
|
433
|
+
|
|
434
|
+
async def place_order(
|
|
435
|
+
self,
|
|
436
|
+
*,
|
|
437
|
+
inst_id: str | None = None,
|
|
438
|
+
symbol: str | None = None,
|
|
439
|
+
side: Literal["buy", "sell"],
|
|
440
|
+
ord_type: Literal["limit", "market", "post_only", "ioc"],
|
|
441
|
+
qty_contract: float | str | None = None,
|
|
442
|
+
qty_base: float | str | None = None,
|
|
443
|
+
price: float | str | None = None,
|
|
444
|
+
td_mode: Literal["isolated", "cross"] = "cross",
|
|
445
|
+
pos_side: Literal["long", "short", "net"] | None = None,
|
|
446
|
+
mrg_position: Literal["merge", "split"] | None = None,
|
|
447
|
+
reduce_only: bool | None = None,
|
|
448
|
+
ccy: str | None = None,
|
|
449
|
+
cl_ord_id: str | None = None,
|
|
450
|
+
tag: str | None = None,
|
|
451
|
+
close_pos_id: str | None = None,
|
|
452
|
+
tgt_ccy: str | None = None,
|
|
453
|
+
tp_trigger_px: float | str | None = None,
|
|
454
|
+
sl_trigger_px: float | str | None = None,
|
|
455
|
+
) -> dict[str, Any]:
|
|
456
|
+
"""``POST /deepcoin/trade/order`` with precision auto-adjustment.
|
|
457
|
+
|
|
458
|
+
{'ordId': '1001113832243662', 'clOrdId': '', 'tag': '', 'sCode': '0', 'sMsg': ''}
|
|
459
|
+
"""
|
|
460
|
+
|
|
461
|
+
resolved_inst_id, detail = await self._resolve_instrument(
|
|
462
|
+
inst_id=inst_id, symbol=symbol
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
lot_step = detail.get("lotSz") or detail.get("step_size")
|
|
466
|
+
tick_step = detail.get("tickSz") or detail.get("tick_size")
|
|
467
|
+
min_size = detail.get("minSz")
|
|
468
|
+
|
|
469
|
+
if qty_contract is None and qty_base is None:
|
|
470
|
+
raise ValueError("Either qty_contract or qty_base must be provided")
|
|
471
|
+
|
|
472
|
+
contract_value_raw = (
|
|
473
|
+
detail.get("ctVal")
|
|
474
|
+
or detail.get("contractValue")
|
|
475
|
+
or detail.get("faceValue")
|
|
476
|
+
or "1"
|
|
477
|
+
)
|
|
478
|
+
try:
|
|
479
|
+
contract_value_dec = Decimal(str(contract_value_raw))
|
|
480
|
+
except Exception:
|
|
481
|
+
contract_value_dec = Decimal("1")
|
|
482
|
+
if contract_value_dec <= 0:
|
|
483
|
+
contract_value_dec = Decimal("1")
|
|
484
|
+
|
|
485
|
+
qty_contract_dec: Decimal | None = None
|
|
486
|
+
|
|
487
|
+
if qty_contract is not None:
|
|
488
|
+
qty_contract_dec = self._to_decimal(qty_contract)
|
|
489
|
+
|
|
490
|
+
if qty_base is not None:
|
|
491
|
+
qty_base_dec = self._to_decimal(qty_base)
|
|
492
|
+
converted = qty_base_dec / contract_value_dec
|
|
493
|
+
if qty_contract_dec is None:
|
|
494
|
+
qty_contract_dec = converted
|
|
495
|
+
elif abs(qty_contract_dec - converted) > Decimal("1e-8"):
|
|
496
|
+
qty_contract_dec = converted
|
|
497
|
+
|
|
498
|
+
if qty_contract_dec is None:
|
|
499
|
+
raise ValueError("Unable to determine qty_contract from inputs")
|
|
500
|
+
|
|
501
|
+
qty_contract_dec = self._quantize(qty_contract_dec, lot_step, ROUND_DOWN)
|
|
502
|
+
if min_size is not None:
|
|
503
|
+
min_dec = self._to_decimal(min_size)
|
|
504
|
+
if qty_contract_dec < min_dec:
|
|
505
|
+
qty_contract_dec = self._ceil_to_step(min_dec, lot_step)
|
|
506
|
+
if qty_contract_dec <= 0:
|
|
507
|
+
raise ValueError("qty_contract is too small after precision adjustment")
|
|
508
|
+
|
|
509
|
+
payload: dict[str, Any] = {
|
|
510
|
+
"instId": resolved_inst_id,
|
|
511
|
+
"tdMode": td_mode,
|
|
512
|
+
"side": side,
|
|
513
|
+
"ordType": ord_type,
|
|
514
|
+
"sz": self._decimal_to_str(qty_contract_dec),
|
|
515
|
+
}
|
|
516
|
+
if detail.get("instType"):
|
|
517
|
+
payload["instType"] = detail["instType"]
|
|
518
|
+
|
|
519
|
+
inst_id_upper = resolved_inst_id.upper()
|
|
520
|
+
requires_pos = inst_id_upper.endswith("-SWAP")
|
|
521
|
+
if requires_pos:
|
|
522
|
+
if pos_side is None:
|
|
523
|
+
effective_pos = "long" if side == "buy" else "short"
|
|
524
|
+
else:
|
|
525
|
+
effective_pos = pos_side
|
|
526
|
+
payload["posSide"] = effective_pos
|
|
527
|
+
if mrg_position or td_mode == "cross":
|
|
528
|
+
payload["mrgPosition"] = mrg_position or "merge"
|
|
529
|
+
elif pos_side is not None:
|
|
530
|
+
payload["posSide"] = pos_side
|
|
531
|
+
if mrg_position and not requires_pos:
|
|
532
|
+
payload["mrgPosition"] = mrg_position
|
|
533
|
+
|
|
534
|
+
if ord_type in {"limit", "post_only"}:
|
|
535
|
+
if price is None:
|
|
536
|
+
raise ValueError("price is required for limit/post_only orders")
|
|
537
|
+
price_dec = self._quantize(
|
|
538
|
+
self._to_decimal(price), tick_step, ROUND_HALF_UP
|
|
539
|
+
)
|
|
540
|
+
if price_dec <= 0:
|
|
541
|
+
raise ValueError("price must be positive after precision adjustment")
|
|
542
|
+
payload["px"] = self._decimal_to_str(price_dec)
|
|
543
|
+
elif price is not None:
|
|
544
|
+
price_dec = self._quantize(
|
|
545
|
+
self._to_decimal(price), tick_step, ROUND_HALF_UP
|
|
546
|
+
)
|
|
547
|
+
if price_dec > 0:
|
|
548
|
+
payload["px"] = self._decimal_to_str(price_dec)
|
|
549
|
+
|
|
550
|
+
if reduce_only is not None:
|
|
551
|
+
payload["reduceOnly"] = bool(reduce_only)
|
|
552
|
+
if ccy:
|
|
553
|
+
payload["ccy"] = ccy
|
|
554
|
+
if cl_ord_id:
|
|
555
|
+
payload["clOrdId"] = cl_ord_id
|
|
556
|
+
if tag:
|
|
557
|
+
payload["tag"] = tag
|
|
558
|
+
if close_pos_id:
|
|
559
|
+
payload["closePosId"] = close_pos_id
|
|
560
|
+
if tgt_ccy:
|
|
561
|
+
payload["tgtCcy"] = tgt_ccy
|
|
562
|
+
|
|
563
|
+
if tp_trigger_px is not None:
|
|
564
|
+
tp_dec = self._quantize(
|
|
565
|
+
self._to_decimal(tp_trigger_px), tick_step, ROUND_HALF_UP
|
|
566
|
+
)
|
|
567
|
+
payload["tpTriggerPx"] = self._decimal_to_str(tp_dec)
|
|
568
|
+
if sl_trigger_px is not None:
|
|
569
|
+
sl_dec = self._quantize(
|
|
570
|
+
self._to_decimal(sl_trigger_px), tick_step, ROUND_HALF_UP
|
|
571
|
+
)
|
|
572
|
+
payload["slTriggerPx"] = self._decimal_to_str(sl_dec)
|
|
573
|
+
|
|
574
|
+
res = await self.client.post(
|
|
575
|
+
f"{self.rest_api}/deepcoin/trade/order",
|
|
576
|
+
data=payload,
|
|
577
|
+
)
|
|
578
|
+
data:dict = await res.json()
|
|
579
|
+
code = data.get("code", '0')
|
|
580
|
+
if code != '0':
|
|
581
|
+
raise RuntimeError(f"Failed to place order: {data.get('msg','')}")
|
|
582
|
+
|
|
583
|
+
data = data.get("data", {})
|
|
584
|
+
sccode = str(data.get("sCode", ""))
|
|
585
|
+
smsg = data.get("sMsg", "")
|
|
586
|
+
if sccode != "0":
|
|
587
|
+
raise RuntimeError(f"Failed to place order: {sccode} {smsg}")
|
|
588
|
+
return data
|
|
589
|
+
|
|
590
|
+
async def cancel_order(
|
|
591
|
+
self,
|
|
592
|
+
*,
|
|
593
|
+
inst_id: str | None = None,
|
|
594
|
+
symbol: str | None = None,
|
|
595
|
+
ord_id: str,
|
|
596
|
+
) -> dict[str, Any]:
|
|
597
|
+
resolved_inst_id, _ = await self._resolve_instrument(
|
|
598
|
+
inst_id=inst_id, symbol=symbol
|
|
599
|
+
)
|
|
600
|
+
payload = {"instId": resolved_inst_id, "ordId": ord_id}
|
|
601
|
+
res = await self.client.post(
|
|
602
|
+
f"{self.rest_api}/deepcoin/trade/cancel-order",
|
|
603
|
+
data=payload,
|
|
604
|
+
)
|
|
605
|
+
resp = await res.json()
|
|
606
|
+
data = resp.get("data", {})
|
|
607
|
+
sc_code = str(data.get("sCode", ""))
|
|
608
|
+
if sc_code != "0":
|
|
609
|
+
raise RuntimeError(f"Failed to cancel order: {resp}")
|
|
610
|
+
return data
|
|
611
|
+
|
|
612
|
+
async def get_price_list(
|
|
613
|
+
self,
|
|
614
|
+
) -> dict[str, Any]:
|
|
615
|
+
|
|
616
|
+
"""
|
|
617
|
+
返回值示例:
|
|
618
|
+
.. code :: json
|
|
619
|
+
{
|
|
620
|
+
"code": 0,
|
|
621
|
+
"msg": "OK",
|
|
622
|
+
"data": [
|
|
623
|
+
{
|
|
624
|
+
"ProductGroup": "SwapU",
|
|
625
|
+
"InstrumentID": "LAYERUSDT",
|
|
626
|
+
"OpenPrice": 0.2015,
|
|
627
|
+
"LastPrice": 0.2039,
|
|
628
|
+
"MarkedPrice": 0.204,
|
|
629
|
+
"LowerLimitPrice": 0.1022,
|
|
630
|
+
"UpperLimitPrice": 0.3065,
|
|
631
|
+
"HighestPrice": 0.2085,
|
|
632
|
+
"LowestPrice": 0.1929,
|
|
633
|
+
"Volume": 9747616,
|
|
634
|
+
"Turnover": 1961292.57819997,
|
|
635
|
+
"AskPrice1": 0.204,
|
|
636
|
+
"BidPrice1": 0.2039,
|
|
637
|
+
"Volume24": 13529402,
|
|
638
|
+
"Turnover24": 2721574.44530007
|
|
639
|
+
}
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
# https://www.deepcoin.com/v2/public/query/swap/price-list?system=SwapU
|
|
645
|
+
|
|
646
|
+
res = await self.client.get(
|
|
647
|
+
f"{self.rest_api}/v2/public/query/swap/price-list",
|
|
648
|
+
params={"system": "SwapU"},
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
return await res.json()
|