hyperquant 0.5__py3-none-any.whl → 0.7__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 +101 -0
- hyperquant/broker/edgex.py +500 -0
- hyperquant/broker/lbank.py +354 -0
- hyperquant/broker/lib/edgex_sign.py +455 -0
- hyperquant/broker/lib/util.py +22 -0
- hyperquant/broker/models/bitget.py +283 -0
- hyperquant/broker/models/edgex.py +1053 -0
- hyperquant/broker/models/lbank.py +547 -0
- hyperquant/broker/models/ourbit.py +184 -135
- hyperquant/broker/ourbit.py +16 -5
- hyperquant/broker/ws.py +21 -3
- hyperquant/core.py +3 -0
- {hyperquant-0.5.dist-info → hyperquant-0.7.dist-info}/METADATA +1 -1
- hyperquant-0.7.dist-info/RECORD +29 -0
- hyperquant-0.5.dist-info/RECORD +0 -21
- {hyperquant-0.5.dist-info → hyperquant-0.7.dist-info}/WHEEL +0 -0
@@ -0,0 +1,500 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from decimal import Decimal
|
5
|
+
import hashlib
|
6
|
+
import math
|
7
|
+
import random
|
8
|
+
import re
|
9
|
+
import time
|
10
|
+
from typing import Any, Iterable, Literal
|
11
|
+
|
12
|
+
import pybotters
|
13
|
+
|
14
|
+
from .models.edgex import EdgexDataStore
|
15
|
+
from .lib.edgex_sign import LimitOrderMessage, LimitOrderSigner
|
16
|
+
from .lib.util import fmt_value
|
17
|
+
|
18
|
+
def gen_client_id():
|
19
|
+
# 1. 生成 [0,1) 的浮点数
|
20
|
+
r = random.random()
|
21
|
+
# 2. 转成字符串
|
22
|
+
s = str(r) # e.g. "0.123456789"
|
23
|
+
# 3. 去掉 "0."
|
24
|
+
digits = s[2:]
|
25
|
+
# 4. 去掉前导 0
|
26
|
+
digits = re.sub(r"^0+", "", digits)
|
27
|
+
return digits
|
28
|
+
|
29
|
+
|
30
|
+
def calc_nonce(client_order_id: str) -> int:
|
31
|
+
digest = hashlib.sha256(client_order_id.encode()).hexdigest()
|
32
|
+
return int(digest[:8], 16)
|
33
|
+
|
34
|
+
def bignumber_to_string(x: Decimal) -> str:
|
35
|
+
# normalize 去掉尾随零,然后用 f 格式避免科学计数法
|
36
|
+
s = format(x.normalize(), "f")
|
37
|
+
# 去掉小数点后多余的 0
|
38
|
+
if "." in s:
|
39
|
+
s = s.rstrip("0").rstrip(".")
|
40
|
+
return s
|
41
|
+
|
42
|
+
class Edgex:
|
43
|
+
"""
|
44
|
+
Edgex 公共 API (HTTP/WS) 封装。
|
45
|
+
|
46
|
+
说明
|
47
|
+
- 当前仅包含公共行情数据(不包含私有接口)。
|
48
|
+
- 订单簿频道命名规则:``depth.{contractId}.{level}``。
|
49
|
+
成功订阅后,服务器会先推送一次完整快照(depthType=SNAPSHOT),之后持续推送增量(depthType=CHANGED)。
|
50
|
+
解析后的结果存入 ``EdgexDataStore.book``。
|
51
|
+
|
52
|
+
参数
|
53
|
+
- client: ``pybotters.Client`` 实例
|
54
|
+
- api_url: REST 基地址;默认使用 Edgex 官方 testnet 站点
|
55
|
+
- ws_url: WebSocket 基地址;如不提供,则默认使用官方文档地址。
|
56
|
+
"""
|
57
|
+
|
58
|
+
def __init__(
|
59
|
+
self,
|
60
|
+
client: pybotters.Client,
|
61
|
+
*,
|
62
|
+
api_url: str | None = None,
|
63
|
+
) -> None:
|
64
|
+
self.client = client
|
65
|
+
self.store = EdgexDataStore()
|
66
|
+
# 公共端点可能因环境/地区不同而变化,允许外部覆盖。
|
67
|
+
self.api_url = api_url or "https://pro.edgex.exchange"
|
68
|
+
self.ws_url = "wss://quote.edgex.exchange"
|
69
|
+
self.userid = None
|
70
|
+
self.eth_address = None
|
71
|
+
self.l2key = None
|
72
|
+
|
73
|
+
api = self.client._session.__dict__['_apis'].get("edgex") # type: ignore
|
74
|
+
if api:
|
75
|
+
self.l2key = api[2].split("-")[1]
|
76
|
+
|
77
|
+
async def __aenter__(self) -> "Edgex":
|
78
|
+
# 初始化基础合约元数据,便于后续使用 tickSize 等字段。
|
79
|
+
await self.update_detail()
|
80
|
+
await self.sync_user()
|
81
|
+
return self
|
82
|
+
|
83
|
+
async def sync_user(self) -> dict[str, Any]:
|
84
|
+
# https://pro.edgex.exchange/api/v1/private/user/getUserInfo
|
85
|
+
# https://pro.edgex.exchange/api/v1/private/account/getAccountPage?size=100
|
86
|
+
# url = self._resolve_api_path("/api/v1/private/user/getUserInfo")
|
87
|
+
# url = self._resolve_api_path("/api/v1/private/account/getAccountPage")
|
88
|
+
|
89
|
+
res = await self.client.get(f'{self.api_url}/api/v1/private/account/getAccountPage?size=100')
|
90
|
+
|
91
|
+
data = await res.json()
|
92
|
+
|
93
|
+
# 重新取 userId ethAddress accountId
|
94
|
+
data = data.get("data", {})
|
95
|
+
accounts = data.get("dataList", [])
|
96
|
+
if accounts:
|
97
|
+
account = accounts[0]
|
98
|
+
self.userid = account.get("userId")
|
99
|
+
self.eth_address = account.get("ethAddress")
|
100
|
+
self.accountid = account.get("id")
|
101
|
+
else:
|
102
|
+
raise ValueError("No account data found in response")
|
103
|
+
|
104
|
+
async def sub_personal(self) -> None:
|
105
|
+
"""订阅用户相关的私有频道(需要登录)。"""
|
106
|
+
await self.client.ws_connect(
|
107
|
+
f"{self.ws_url}/api/v1/private/ws?accountId={self.accountid}×tamp=" + str(int(time.time() * 1000)),
|
108
|
+
hdlr_json=self.store.onmessage,
|
109
|
+
)
|
110
|
+
|
111
|
+
async def __aexit__(
|
112
|
+
self,
|
113
|
+
exc_type: type[BaseException] | None,
|
114
|
+
exc: BaseException | None,
|
115
|
+
tb: BaseException | None,
|
116
|
+
) -> None:
|
117
|
+
# Edgex 当前没有需要关闭的资源;保持接口与 Ourbit 等类一致。
|
118
|
+
return None
|
119
|
+
|
120
|
+
async def update_detail(self) -> dict[str, Any]:
|
121
|
+
"""Fetch and cache contract metadata via the public REST endpoint."""
|
122
|
+
|
123
|
+
await self.store.initialize(
|
124
|
+
self.client.get(f'{self.api_url}/api/v1/public/meta/getMetaData'),
|
125
|
+
)
|
126
|
+
|
127
|
+
async def update(
|
128
|
+
self,
|
129
|
+
update_type: Literal["balance", "position", "orders", "ticker", "all"] = "all",
|
130
|
+
*,
|
131
|
+
contract_id: str | None = None,
|
132
|
+
) -> None:
|
133
|
+
"""使用 REST 刷新本地缓存的账户资产、持仓、活动订单与 24h 行情。"""
|
134
|
+
|
135
|
+
requires_account = {"balance", "position", "orders", "all"}
|
136
|
+
if update_type in requires_account and not getattr(self, "accountid", None):
|
137
|
+
raise ValueError("accountid not set; call sync_user() before update().")
|
138
|
+
|
139
|
+
account_asset_url = None
|
140
|
+
active_orders_url = None
|
141
|
+
if update_type in requires_account:
|
142
|
+
account_asset_url = (
|
143
|
+
f"{self.api_url}/api/v1/private/account/getAccountAsset"
|
144
|
+
f"?accountId={self.accountid}"
|
145
|
+
)
|
146
|
+
active_orders_url = (
|
147
|
+
f"{self.api_url}/api/v1/private/order/getActiveOrderPage"
|
148
|
+
f"?accountId={self.accountid}&size=200"
|
149
|
+
)
|
150
|
+
|
151
|
+
ticker_url = f"{self.api_url}/api/v1/public/quote/getTicker"
|
152
|
+
if contract_id:
|
153
|
+
ticker_url = f"{ticker_url}?contractId={contract_id}"
|
154
|
+
|
155
|
+
url_map: dict[str, list[str]] = {
|
156
|
+
"balance": [account_asset_url] if account_asset_url else [],
|
157
|
+
"position": [account_asset_url] if account_asset_url else [],
|
158
|
+
"orders": [active_orders_url] if active_orders_url else [],
|
159
|
+
"ticker": [ticker_url],
|
160
|
+
"all": [
|
161
|
+
*(url for url in (account_asset_url, active_orders_url) if url),
|
162
|
+
ticker_url,
|
163
|
+
],
|
164
|
+
}
|
165
|
+
|
166
|
+
try:
|
167
|
+
urls = url_map[update_type]
|
168
|
+
except KeyError:
|
169
|
+
raise ValueError(f"update_type err: {update_type}")
|
170
|
+
|
171
|
+
# 直接传协程进去,initialize 会自己 await
|
172
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
173
|
+
|
174
|
+
|
175
|
+
|
176
|
+
async def sub_orderbook(
|
177
|
+
self,
|
178
|
+
contract_ids: str | Iterable[str] | None = None,
|
179
|
+
*,
|
180
|
+
symbols: str | Iterable[str] | None = None,
|
181
|
+
level: int = 15,
|
182
|
+
ws_url: str | None = None,
|
183
|
+
) -> None:
|
184
|
+
"""订阅指定合约 ID 或交易对名的订单簿(遵循 Edgex 协议)。
|
185
|
+
|
186
|
+
规范
|
187
|
+
- 默认 WS 端点:wss://quote.edgex.exchange(可通过参数/实例覆盖)
|
188
|
+
- 每个频道的订阅报文:
|
189
|
+
{"type": "subscribe", "channel": "depth.{contractId}.{level}"}
|
190
|
+
- 服务端在订阅成功后,会先推送一次快照,再持续推送增量。
|
191
|
+
"""
|
192
|
+
|
193
|
+
ids: list[str] = []
|
194
|
+
if contract_ids is not None:
|
195
|
+
if isinstance(contract_ids, str):
|
196
|
+
ids.extend([contract_ids])
|
197
|
+
else:
|
198
|
+
ids.extend(contract_ids)
|
199
|
+
|
200
|
+
if symbols is not None:
|
201
|
+
if isinstance(symbols, str):
|
202
|
+
lookup_symbols = [symbols]
|
203
|
+
else:
|
204
|
+
lookup_symbols = list(symbols)
|
205
|
+
|
206
|
+
for symbol in lookup_symbols:
|
207
|
+
matches = self.store.detail.find({"contractName": symbol})
|
208
|
+
if not matches:
|
209
|
+
raise ValueError(f"Unknown Edgex symbol: {symbol}")
|
210
|
+
ids.append(str(matches[0]["contractId"]))
|
211
|
+
|
212
|
+
if not ids:
|
213
|
+
raise ValueError("contract_ids or symbols must be provided")
|
214
|
+
|
215
|
+
channels = [f"depth.{cid}.{level}" for cid in ids]
|
216
|
+
|
217
|
+
# 优先使用参数 ws_url,其次使用实例的 ws_url,最后使用默认地址。
|
218
|
+
url = f"{self.ws_url}/api/v1/public/ws?timestamp=" + str(int(time.time() * 1000))
|
219
|
+
|
220
|
+
# 根据文档:每个频道一条订阅指令,允许一次发送多个订阅对象。
|
221
|
+
payload = [{"type": "subscribe", "channel": ch} for ch in channels]
|
222
|
+
|
223
|
+
wsapp = self.client.ws_connect(url, send_json=payload, hdlr_json=self.store.onmessage)
|
224
|
+
# 等待 WS 完成握手再返回,确保订阅报文成功发送。
|
225
|
+
await wsapp._event.wait()
|
226
|
+
|
227
|
+
async def sub_ticker(
|
228
|
+
self,
|
229
|
+
contract_ids: str | Iterable[str] | None = None,
|
230
|
+
*,
|
231
|
+
symbols: str | Iterable[str] | None = None,
|
232
|
+
all_contracts: bool = False,
|
233
|
+
periodic: bool = False,
|
234
|
+
ws_url: str | None = None,
|
235
|
+
) -> None:
|
236
|
+
"""订阅 24 小时行情推送。
|
237
|
+
|
238
|
+
参数
|
239
|
+
- contract_ids / symbols: 指定单个或多个合约;二者至少提供一个。
|
240
|
+
- all_contracts: 订阅 ``ticker.all``(或 ``ticker.all.1s``)。
|
241
|
+
- periodic: 与 ``all_contracts`` 配合,true 则订阅 ``ticker.all.1s``。
|
242
|
+
"""
|
243
|
+
|
244
|
+
channels: list[str] = []
|
245
|
+
|
246
|
+
if all_contracts:
|
247
|
+
channel = "ticker.all.1s" if periodic else "ticker.all"
|
248
|
+
channels.append(channel)
|
249
|
+
else:
|
250
|
+
ids: list[str] = []
|
251
|
+
if contract_ids is not None:
|
252
|
+
if isinstance(contract_ids, str):
|
253
|
+
ids.append(contract_ids)
|
254
|
+
else:
|
255
|
+
ids.extend(contract_ids)
|
256
|
+
|
257
|
+
if symbols is not None:
|
258
|
+
if isinstance(symbols, str):
|
259
|
+
lookup_symbols = [symbols]
|
260
|
+
else:
|
261
|
+
lookup_symbols = list(symbols)
|
262
|
+
|
263
|
+
for symbol in lookup_symbols:
|
264
|
+
matches = self.store.detail.find({"contractName": symbol})
|
265
|
+
if not matches:
|
266
|
+
raise ValueError(f"Unknown Edgex symbol: {symbol}")
|
267
|
+
ids.append(str(matches[0]["contractId"]))
|
268
|
+
|
269
|
+
if not ids:
|
270
|
+
raise ValueError("Provide contract_ids/symbols or set all_contracts=True")
|
271
|
+
|
272
|
+
channels.extend(f"ticker.{cid}" for cid in ids)
|
273
|
+
|
274
|
+
url = ws_url or f"{self.ws_url}/api/v1/public/ws?timestamp=" + str(int(time.time() * 1000))
|
275
|
+
payload = [{"type": "subscribe", "channel": ch} for ch in channels]
|
276
|
+
print(payload)
|
277
|
+
wsapp = self.client.ws_connect(url, send_json=payload, hdlr_json=self.store.onmessage)
|
278
|
+
await wsapp._event.wait()
|
279
|
+
|
280
|
+
def _fmt_price(self, symbol: str, price: float) -> str:
|
281
|
+
o = self.store.detail.get({"contractName": symbol})
|
282
|
+
if not o:
|
283
|
+
raise ValueError(f"Unknown Edgex symbol: {symbol}")
|
284
|
+
tick = float(o.get("tickSize"))
|
285
|
+
return fmt_value(price, float(tick))
|
286
|
+
|
287
|
+
def _fmt_size(self, symbol: str, size: float) -> str:
|
288
|
+
o = self.store.detail.get({"contractName": symbol})
|
289
|
+
if not o:
|
290
|
+
raise ValueError(f"Unknown Edgex symbol: {symbol}")
|
291
|
+
step = float(o.get("stepSize"))
|
292
|
+
return fmt_value(size, float(step))
|
293
|
+
|
294
|
+
|
295
|
+
async def place_order(
|
296
|
+
self,
|
297
|
+
symbol: str,
|
298
|
+
side: Literal["buy", "sell"],
|
299
|
+
price: float = None,
|
300
|
+
quantity: float = None,
|
301
|
+
order_type: Literal["market", "limit_ioc", "limit_gtc"] = "limit_ioc",
|
302
|
+
usdt_amount: float = None,
|
303
|
+
):
|
304
|
+
"""下单接口(私有 REST)。
|
305
|
+
返回值order_id: str
|
306
|
+
"""
|
307
|
+
|
308
|
+
# 前端请求模板
|
309
|
+
args = {
|
310
|
+
"price": "210.00",
|
311
|
+
"size": "1.0",
|
312
|
+
"type": "LIMIT",
|
313
|
+
"timeInForce": "GOOD_TIL_CANCEL",
|
314
|
+
"reduceOnly": False,
|
315
|
+
"isPositionTpsl": False,
|
316
|
+
"isSetOpenTp": False,
|
317
|
+
"isSetOpenSl": False,
|
318
|
+
"accountId": "663528067938910372",
|
319
|
+
"contractId": "10000003",
|
320
|
+
"side": "BUY",
|
321
|
+
"triggerPrice": "",
|
322
|
+
"triggerPriceType": "LAST_PRICE",
|
323
|
+
"clientOrderId": "39299826149407513",
|
324
|
+
"expireTime": "1760352231536",
|
325
|
+
"l2Nonce": "1872301",
|
326
|
+
"l2Value": "210",
|
327
|
+
"l2Size": "1.0",
|
328
|
+
"l2LimitFee": "1",
|
329
|
+
"l2ExpireTime": "1761129831536",
|
330
|
+
"l2Signature": "03c4d84c30586b12ab9fec939a875201e58dac9a0391f15eb6118ab2fb50464804ce38b19cc5e07c973fc66b449bec0274058ea2d012c1c7a580f805d2c7a1d3",
|
331
|
+
"extraType": "",
|
332
|
+
"extraDataJson": "",
|
333
|
+
"symbol": "SOLUSD",
|
334
|
+
"showEqualValInput": False,
|
335
|
+
"maxSellQTY": 1, # 不需要特别计算, 服务器不校验
|
336
|
+
"maxBuyQTY": 1 # 不需要特别计算, 服务器不校验
|
337
|
+
}
|
338
|
+
|
339
|
+
try:
|
340
|
+
size = Decimal(self._fmt_size(symbol, quantity))
|
341
|
+
price = Decimal(self._fmt_price(symbol, price))
|
342
|
+
except (ValueError, TypeError):
|
343
|
+
raise ValueError("failed to parse size or price")
|
344
|
+
|
345
|
+
if 'gtc' in order_type:
|
346
|
+
args['timeInForce'] = "GOOD_TIL_CANCEL"
|
347
|
+
if 'ioc' in order_type:
|
348
|
+
args['timeInForce'] = "IMMEDIATE_OR_CANCEL"
|
349
|
+
if 'limit' in order_type:
|
350
|
+
args['type'] = "LIMIT"
|
351
|
+
if 'market' in order_type:
|
352
|
+
args['type'] = "MARKET"
|
353
|
+
|
354
|
+
if side == 'buy':
|
355
|
+
price = price * 10
|
356
|
+
else:
|
357
|
+
tick_size = self.store.detail.get({'contractName': symbol}).get("tickSize")
|
358
|
+
price = Decimal(tick_size)
|
359
|
+
|
360
|
+
if not self.l2key or not self.userid:
|
361
|
+
raise ValueError("L2 key or userId is not set. Ensure API keys are correctly configured.")
|
362
|
+
|
363
|
+
|
364
|
+
collateral_coin = self.store.app.get({'appName': 'edgeX'})
|
365
|
+
|
366
|
+
c = self.store.detail.get({'contractName': symbol})
|
367
|
+
if not c:
|
368
|
+
raise ValueError(f"Unknown Edgex symbol: {symbol}")
|
369
|
+
hex_resolution = c.get("starkExResolution", "0x0")
|
370
|
+
hex_resolution = hex_resolution.replace("0x", "")
|
371
|
+
|
372
|
+
try:
|
373
|
+
resolution_int = int(hex_resolution, 16)
|
374
|
+
resolution = Decimal(resolution_int)
|
375
|
+
except (ValueError, TypeError):
|
376
|
+
raise ValueError("failed to parse hex resolution")
|
377
|
+
|
378
|
+
client_order_id = gen_client_id()
|
379
|
+
|
380
|
+
# Calculate values
|
381
|
+
value_dm:Decimal = price * size
|
382
|
+
|
383
|
+
amount_synthetic = int(size * resolution)
|
384
|
+
amount_collateral = int(value_dm * Decimal("1000000")) # Shift 6 decimal places
|
385
|
+
|
386
|
+
|
387
|
+
# Calculate fee based on order type (maker/taker)
|
388
|
+
try:
|
389
|
+
fee_rate = Decimal(c.get("defaultTakerFeeRate", "0"))
|
390
|
+
except (ValueError, TypeError):
|
391
|
+
raise ValueError("failed to parse fee rate")
|
392
|
+
|
393
|
+
# Calculate fee amount in decimal with ceiling to integer
|
394
|
+
amount_fee_dm = Decimal(str(math.ceil(float(value_dm * fee_rate))))
|
395
|
+
amount_fee_str = str(amount_fee_dm)
|
396
|
+
|
397
|
+
# Convert to the required integer format for the protocol
|
398
|
+
amount_fee = int(amount_fee_dm * Decimal("1000000")) # Shift 6 decimal places
|
399
|
+
|
400
|
+
nonce = calc_nonce(client_order_id)
|
401
|
+
now = int(time.time() * 1000)
|
402
|
+
l2_expire_time = int(now + 2592e6) # 30 天后
|
403
|
+
expireTime = int(l2_expire_time - 7776e5) # 提前 9 天
|
404
|
+
|
405
|
+
|
406
|
+
# Calculate signature using asset IDs from metadata
|
407
|
+
expire_time_unix = int(l2_expire_time // (60 * 60 * 1000))
|
408
|
+
|
409
|
+
asset_id_synthetic = c.get("starkExSyntheticAssetId")
|
410
|
+
|
411
|
+
act_id = self.accountid
|
412
|
+
|
413
|
+
message = LimitOrderMessage(
|
414
|
+
asset_id_synthetic=asset_id_synthetic, # SOLUSD
|
415
|
+
asset_id_collateral=collateral_coin.get("starkExCollateralStarkExAssetId"), # USDT
|
416
|
+
asset_id_fee=collateral_coin.get("starkExCollateralStarkExAssetId"),
|
417
|
+
is_buy= side=='buy', # isBuyingSynthetic
|
418
|
+
amount_synthetic=amount_synthetic, # quantumsAmountSynthetic
|
419
|
+
amount_collateral=amount_collateral, # quantumsAmountCollateral
|
420
|
+
amount_fee=amount_fee, # quantumsAmountFee
|
421
|
+
nonce=int(nonce), # nonce
|
422
|
+
position_id=int(act_id), # positionId
|
423
|
+
expiration_epoch_hours=int(expire_time_unix), # 此处也比较重要 # TODO: 计算
|
424
|
+
)
|
425
|
+
|
426
|
+
# 取 L2 私钥
|
427
|
+
|
428
|
+
|
429
|
+
signer = LimitOrderSigner(self.l2key)
|
430
|
+
hash_hex, signature_hex = signer.sign(message)
|
431
|
+
value_str = bignumber_to_string(value_dm)
|
432
|
+
|
433
|
+
price_str = str(price) if 'limit' in order_type else "0"
|
434
|
+
|
435
|
+
args.update({
|
436
|
+
'price': price_str,
|
437
|
+
'size': str(float(size)),
|
438
|
+
'side': side.upper(),
|
439
|
+
'accountId': str(act_id),
|
440
|
+
'contractId': str(c.get("contractId")),
|
441
|
+
'clientOrderId': client_order_id,
|
442
|
+
'expireTime': str(expireTime),
|
443
|
+
'l2ExpireTime': str(l2_expire_time),
|
444
|
+
'l2Nonce': str(nonce),
|
445
|
+
'l2Value': value_str,
|
446
|
+
'l2Size': str(float(size)),
|
447
|
+
'l2LimitFee': amount_fee_str,
|
448
|
+
'l2Signature': signature_hex,
|
449
|
+
'symbol': symbol
|
450
|
+
})
|
451
|
+
|
452
|
+
|
453
|
+
|
454
|
+
res = await self.client.post(
|
455
|
+
f'{self.api_url}/api/v1/private/order/createOrder',
|
456
|
+
data=args
|
457
|
+
)
|
458
|
+
|
459
|
+
data:dict = await res.json()
|
460
|
+
if data.get("code") != "SUCCESS": # pragma: no cover - defensive guard
|
461
|
+
raise RuntimeError(f"Failed to place Edgex order: {data}")
|
462
|
+
|
463
|
+
latency = int(data.get("responseTime",0)) - int(data.get("requestTime",0))
|
464
|
+
print(latency)
|
465
|
+
order_id = data.get("data", {}).get("orderId")
|
466
|
+
return order_id
|
467
|
+
|
468
|
+
async def cancel_orders(self, order_ids: list[str]) -> dict[str, Any]:
|
469
|
+
"""
|
470
|
+
批量撤单接口(私有 REST)。
|
471
|
+
|
472
|
+
.. code:: json
|
473
|
+
{
|
474
|
+
"665186247567737508": "SUCCESS"
|
475
|
+
}
|
476
|
+
"""
|
477
|
+
|
478
|
+
args = {
|
479
|
+
"orderIdList": order_ids,
|
480
|
+
"accountId": str(self.accountid),
|
481
|
+
}
|
482
|
+
res = await self.client.post(
|
483
|
+
f'{self.api_url}/api/v1/private/order/cancelOrderById',
|
484
|
+
data=args
|
485
|
+
)
|
486
|
+
data: dict = await res.json()
|
487
|
+
print(data)
|
488
|
+
if data.get("code") != "SUCCESS": # pragma: no cover - defensive guard
|
489
|
+
raise RuntimeError(f"Failed to cancel Edgex orders: {data}")
|
490
|
+
return data.get("data", {}).get("cancelResultMap", {})
|
491
|
+
|
492
|
+
|
493
|
+
async def __aexit__(
|
494
|
+
self,
|
495
|
+
exc_type: type[BaseException] | None,
|
496
|
+
exc: BaseException | None,
|
497
|
+
tb: BaseException | None,
|
498
|
+
) -> None:
|
499
|
+
# Edgex 当前没有需要关闭的资源;保持接口与 Ourbit 等类一致。
|
500
|
+
return None
|