hyperquant 0.93__py3-none-any.whl → 0.94__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 +55 -0
- hyperquant/broker/bitmart.py +471 -0
- hyperquant/broker/lighter.py +470 -0
- hyperquant/broker/models/bitmart.py +487 -0
- hyperquant/broker/models/lighter.py +508 -0
- {hyperquant-0.93.dist-info → hyperquant-0.94.dist-info}/METADATA +2 -1
- {hyperquant-0.93.dist-info → hyperquant-0.94.dist-info}/RECORD +8 -4
- {hyperquant-0.93.dist-info → hyperquant-0.94.dist-info}/WHEEL +0 -0
hyperquant/broker/auth.py
CHANGED
|
@@ -198,6 +198,57 @@ class Auth:
|
|
|
198
198
|
|
|
199
199
|
return args
|
|
200
200
|
|
|
201
|
+
@staticmethod
|
|
202
|
+
def bitmart(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
|
203
|
+
method: str = args[0]
|
|
204
|
+
url: URL = args[1]
|
|
205
|
+
headers: CIMultiDict = kwargs["headers"]
|
|
206
|
+
|
|
207
|
+
session = kwargs["session"]
|
|
208
|
+
host_key = url.host
|
|
209
|
+
try:
|
|
210
|
+
api_name = pybotters.auth.Hosts.items[host_key].name
|
|
211
|
+
except (KeyError, AttributeError):
|
|
212
|
+
api_name = host_key
|
|
213
|
+
|
|
214
|
+
creds = session.__dict__.get("_apis", {}).get(api_name)
|
|
215
|
+
if not creds or len(creds) < 3:
|
|
216
|
+
raise RuntimeError("Bitmart credentials (accessKey, accessSalt, device) are required")
|
|
217
|
+
|
|
218
|
+
access_key = creds[0]
|
|
219
|
+
access_salt = creds[1]
|
|
220
|
+
access_salt = access_salt.decode("utf-8")
|
|
221
|
+
device = creds[2]
|
|
222
|
+
extra_cookie = creds[3] if len(creds) > 3 else None
|
|
223
|
+
|
|
224
|
+
cookie_parts = [
|
|
225
|
+
f"accessKey={access_key}",
|
|
226
|
+
f"accessSalt={access_salt}",
|
|
227
|
+
"hasDelegation=false",
|
|
228
|
+
"delegationType=0",
|
|
229
|
+
"delegationTypeList=[]",
|
|
230
|
+
]
|
|
231
|
+
if extra_cookie:
|
|
232
|
+
if isinstance(extra_cookie, str) and extra_cookie:
|
|
233
|
+
cookie_parts.append(extra_cookie.strip(";"))
|
|
234
|
+
|
|
235
|
+
headers["cookie"] = "; ".join(cookie_parts)
|
|
236
|
+
|
|
237
|
+
headers.setdefault("x-bm-client", "WEB")
|
|
238
|
+
headers.setdefault("x-bm-contract", "2")
|
|
239
|
+
headers.setdefault("x-bm-device", device)
|
|
240
|
+
headers.setdefault("x-bm-timezone", "Asia/Shanghai")
|
|
241
|
+
headers.setdefault("x-bm-timezone-offset", "-480")
|
|
242
|
+
headers.setdefault("x-bm-tag", "")
|
|
243
|
+
headers.setdefault("x-bm-version", "5e13905")
|
|
244
|
+
headers.setdefault('User-Agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0')
|
|
245
|
+
|
|
246
|
+
ua = headers.get("User-Agent") or headers.get("user-agent")
|
|
247
|
+
if ua:
|
|
248
|
+
headers.setdefault("x-bm-ua", ua)
|
|
249
|
+
|
|
250
|
+
return args
|
|
251
|
+
|
|
201
252
|
@staticmethod
|
|
202
253
|
def coinw(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
|
203
254
|
method: str = args[0]
|
|
@@ -294,3 +345,7 @@ pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
|
|
|
294
345
|
pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
|
|
295
346
|
"coinw", Auth.coinw
|
|
296
347
|
)
|
|
348
|
+
|
|
349
|
+
pybotters.auth.Hosts.items["derivatives.bitmart.com"] = pybotters.auth.Item(
|
|
350
|
+
"bitmart", Auth.bitmart
|
|
351
|
+
)
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Literal, Sequence
|
|
8
|
+
|
|
9
|
+
import pybotters
|
|
10
|
+
|
|
11
|
+
from .models.bitmart import BitmartDataStore
|
|
12
|
+
from .lib.util import fmt_value
|
|
13
|
+
|
|
14
|
+
class Bitmart:
|
|
15
|
+
"""Bitmart 合约交易(REST + WebSocket)。"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
client: pybotters.Client,
|
|
20
|
+
*,
|
|
21
|
+
public_api: str | None = None,
|
|
22
|
+
forward_api: str | None = None,
|
|
23
|
+
ws_url: str | None = None,
|
|
24
|
+
account_index: int | None = None,
|
|
25
|
+
apis: str = None
|
|
26
|
+
) -> None:
|
|
27
|
+
self.client = client
|
|
28
|
+
self.store = BitmartDataStore()
|
|
29
|
+
|
|
30
|
+
self.public_api = public_api or "https://contract-v2.bitmart.com"
|
|
31
|
+
self.private_api = "https://derivatives.bitmart.com"
|
|
32
|
+
self.forward_api = f'{self.private_api}/gw-api/contract-tiger/forward'
|
|
33
|
+
self.ws_url = ws_url or "wss://contract-ws-v2.bitmart.com/v1/ifcontract/realTime"
|
|
34
|
+
|
|
35
|
+
self.account_index = account_index
|
|
36
|
+
self.apis = apis
|
|
37
|
+
|
|
38
|
+
async def __aenter__(self) -> "Bitmart":
|
|
39
|
+
await self.update("detail")
|
|
40
|
+
asyncio.create_task(self.auto_refresh())
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
async def auto_refresh(self, sec=3600, test=False) -> None:
|
|
44
|
+
"""每隔一小时刷新token"""
|
|
45
|
+
client = self.client
|
|
46
|
+
while not client._session.closed:
|
|
47
|
+
|
|
48
|
+
await asyncio.sleep(sec)
|
|
49
|
+
|
|
50
|
+
if client._session.__dict__["_apis"].get("bitmart") is None:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# 执行请求
|
|
54
|
+
res = await client.post(
|
|
55
|
+
f"{self.private_api}/gw-api/gateway/token/v2/renew",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
print(await res.text())
|
|
59
|
+
resp:dict = await res.json()
|
|
60
|
+
if resp.get("success") is False:
|
|
61
|
+
raise ValueError(f"Bitmart refreshToken error: {resp}")
|
|
62
|
+
|
|
63
|
+
data:dict = resp.get("data", {})
|
|
64
|
+
new_token = data.get("accessToken")
|
|
65
|
+
secret = data.get("accessSalt")
|
|
66
|
+
|
|
67
|
+
# 加载原来的apis
|
|
68
|
+
apis_dict = client._load_apis(self.apis)
|
|
69
|
+
|
|
70
|
+
device = apis_dict['bitmart'][2]
|
|
71
|
+
|
|
72
|
+
apis_dict["bitmart"] = [new_token, secret, device]
|
|
73
|
+
|
|
74
|
+
client._session.__dict__["_apis"] = client._encode_apis(apis_dict)
|
|
75
|
+
|
|
76
|
+
if test:
|
|
77
|
+
print("Bitmart token refreshed.")
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def get_contract_id(self, symbol: str) -> str | None:
|
|
84
|
+
"""Resolve contract ID from cached detail data."""
|
|
85
|
+
detail = (
|
|
86
|
+
self.store.detail.get({"name": symbol})
|
|
87
|
+
or self.store.detail.get({"display_name": symbol})
|
|
88
|
+
or self.store.detail.get({"contract_id": symbol})
|
|
89
|
+
)
|
|
90
|
+
if detail is None:
|
|
91
|
+
return None
|
|
92
|
+
contract_id = detail.get("contract_id")
|
|
93
|
+
if contract_id is None:
|
|
94
|
+
return None
|
|
95
|
+
return str(contract_id)
|
|
96
|
+
|
|
97
|
+
def _get_detail_entry(
|
|
98
|
+
self,
|
|
99
|
+
*,
|
|
100
|
+
symbol: str | None = None,
|
|
101
|
+
market_index: int | None = None,
|
|
102
|
+
) -> dict[str, Any] | None:
|
|
103
|
+
if symbol:
|
|
104
|
+
entry = (
|
|
105
|
+
self.store.detail.get({"name": symbol})
|
|
106
|
+
or self.store.detail.get({"display_name": symbol})
|
|
107
|
+
)
|
|
108
|
+
if entry:
|
|
109
|
+
return entry
|
|
110
|
+
|
|
111
|
+
if market_index is not None:
|
|
112
|
+
entries = self.store.detail.find({"contract_id": market_index})
|
|
113
|
+
if entries:
|
|
114
|
+
return entries[0]
|
|
115
|
+
entries = self.store.detail.find({"contract_id": str(market_index)})
|
|
116
|
+
if entries:
|
|
117
|
+
return entries[0]
|
|
118
|
+
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _normalize_enum(
|
|
123
|
+
value: int | str,
|
|
124
|
+
mapping: dict[str, int],
|
|
125
|
+
field: str,
|
|
126
|
+
) -> int:
|
|
127
|
+
if isinstance(value, str):
|
|
128
|
+
key = value.lower()
|
|
129
|
+
try:
|
|
130
|
+
return mapping[key]
|
|
131
|
+
except KeyError as exc:
|
|
132
|
+
raise ValueError(f"Unsupported {field}: {value}") from exc
|
|
133
|
+
try:
|
|
134
|
+
return int(value)
|
|
135
|
+
except (TypeError, ValueError) as exc:
|
|
136
|
+
raise ValueError(f"Unsupported {field}: {value}") from exc
|
|
137
|
+
|
|
138
|
+
async def update(
|
|
139
|
+
self,
|
|
140
|
+
update_type: Literal[
|
|
141
|
+
"detail",
|
|
142
|
+
"orders",
|
|
143
|
+
"positions",
|
|
144
|
+
"balances",
|
|
145
|
+
"account",
|
|
146
|
+
"all",
|
|
147
|
+
"history_orders",
|
|
148
|
+
] = "all",
|
|
149
|
+
*,
|
|
150
|
+
orders_params: dict[str, Any] | None = None,
|
|
151
|
+
positions_params: dict[str, Any] | None = None,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Refresh cached REST resources."""
|
|
154
|
+
|
|
155
|
+
tasks: dict[str, Any] = {}
|
|
156
|
+
|
|
157
|
+
include_detail = update_type in {"detail", "all"}
|
|
158
|
+
include_orders = update_type in {"orders", "all"}
|
|
159
|
+
include_positions = update_type in {"positions", "all"}
|
|
160
|
+
include_balances = update_type in {"balances", "account", "all"}
|
|
161
|
+
include_history_orders = update_type in {"history_orders"}
|
|
162
|
+
|
|
163
|
+
if include_detail:
|
|
164
|
+
tasks["detail"] = self.client.get(f"{self.public_api}/v1/ifcontract/contracts_all")
|
|
165
|
+
|
|
166
|
+
if include_orders:
|
|
167
|
+
params = {
|
|
168
|
+
"status": 3,
|
|
169
|
+
"size": 200,
|
|
170
|
+
"orderType": 0,
|
|
171
|
+
"offset": 0,
|
|
172
|
+
"direction": 0,
|
|
173
|
+
"type": 1,
|
|
174
|
+
}
|
|
175
|
+
if orders_params:
|
|
176
|
+
params.update(orders_params)
|
|
177
|
+
tasks["orders"] = self.client.get(
|
|
178
|
+
f"{self.forward_api}/v1/ifcontract/userAllOrders",
|
|
179
|
+
params=params,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if include_positions:
|
|
183
|
+
params = {"status": 1}
|
|
184
|
+
if positions_params:
|
|
185
|
+
params.update(positions_params)
|
|
186
|
+
tasks["positions"] = self.client.get(
|
|
187
|
+
f"{self.forward_api}/v1/ifcontract/userPositions",
|
|
188
|
+
params=params,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if include_balances:
|
|
192
|
+
tasks["balances"] = self.client.get(
|
|
193
|
+
f"{self.forward_api}/v1/ifcontract/copy/trade/user/info",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if include_history_orders:
|
|
197
|
+
d_params = {"offset": 0, "status": 60, "size": 20, "type": 1}
|
|
198
|
+
d_params.update(orders_params or {})
|
|
199
|
+
tasks["history_orders"] = self.client.get(
|
|
200
|
+
f"{self.forward_api}/v1/ifcontract/userAllOrders",
|
|
201
|
+
params=d_params,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
if not tasks:
|
|
205
|
+
raise ValueError(f"Unsupported update_type: {update_type}")
|
|
206
|
+
|
|
207
|
+
results: dict[str, Any] = {}
|
|
208
|
+
for key, req in tasks.items():
|
|
209
|
+
res = await req
|
|
210
|
+
if res.content_type and "json" in res.content_type:
|
|
211
|
+
results[key] = await res.json()
|
|
212
|
+
else:
|
|
213
|
+
text = await res.text()
|
|
214
|
+
try:
|
|
215
|
+
results[key] = json.loads(text)
|
|
216
|
+
except json.JSONDecodeError as exc:
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f"Unexpected response format for {key}: {res.content_type} {text[:200]}"
|
|
219
|
+
) from exc
|
|
220
|
+
|
|
221
|
+
if "detail" in results:
|
|
222
|
+
resp = results["detail"]
|
|
223
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
224
|
+
raise ValueError(f"Bitmart detail API error: {resp}")
|
|
225
|
+
self.store.detail._onresponse(resp)
|
|
226
|
+
for entry in self.store.detail.find():
|
|
227
|
+
contract_id = entry.get("contract_id")
|
|
228
|
+
symbol = entry.get("name") or entry.get("display_name")
|
|
229
|
+
if contract_id is None or symbol is None:
|
|
230
|
+
continue
|
|
231
|
+
self.store.book.id_to_symbol[str(contract_id)] = str(symbol)
|
|
232
|
+
|
|
233
|
+
if "orders" in results:
|
|
234
|
+
resp = results["orders"]
|
|
235
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
236
|
+
raise ValueError(f"Bitmart orders API error: {resp}")
|
|
237
|
+
self.store.orders._onresponse(resp)
|
|
238
|
+
|
|
239
|
+
if "positions" in results:
|
|
240
|
+
resp = results["positions"]
|
|
241
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
242
|
+
raise ValueError(f"Bitmart positions API error: {resp}")
|
|
243
|
+
self.store.positions._onresponse(resp)
|
|
244
|
+
|
|
245
|
+
if "balances" in results:
|
|
246
|
+
resp = results["balances"]
|
|
247
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
248
|
+
raise ValueError(f"Bitmart balances API error: {resp}")
|
|
249
|
+
self.store.balances._onresponse(resp)
|
|
250
|
+
|
|
251
|
+
if "history_orders" in results:
|
|
252
|
+
resp = results["history_orders"]
|
|
253
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
254
|
+
raise ValueError(f"Bitmart history_orders API error: {resp}")
|
|
255
|
+
self.store.orders._onresponse(resp)
|
|
256
|
+
|
|
257
|
+
async def sub_orderbook(
|
|
258
|
+
self,
|
|
259
|
+
symbols: Sequence[str] | str,
|
|
260
|
+
*,
|
|
261
|
+
depth: str = "Depth",
|
|
262
|
+
depth_limit: int | None = None,
|
|
263
|
+
) -> pybotters.ws.WebSocketApp:
|
|
264
|
+
"""Subscribe order book channel(s)."""
|
|
265
|
+
|
|
266
|
+
if isinstance(symbols, str):
|
|
267
|
+
symbols = [symbols]
|
|
268
|
+
|
|
269
|
+
if not symbols:
|
|
270
|
+
raise ValueError("symbols must not be empty")
|
|
271
|
+
|
|
272
|
+
missing = [sym for sym in symbols if self.get_contract_id(sym) is None]
|
|
273
|
+
if missing:
|
|
274
|
+
await self.update("detail")
|
|
275
|
+
still_missing = [sym for sym in missing if self.get_contract_id(sym) is None]
|
|
276
|
+
if still_missing:
|
|
277
|
+
raise ValueError(f"Unknown symbols: {', '.join(still_missing)}")
|
|
278
|
+
|
|
279
|
+
if depth_limit is not None:
|
|
280
|
+
self.store.book.limit = depth_limit
|
|
281
|
+
|
|
282
|
+
channels: list[str] = []
|
|
283
|
+
for symbol in symbols:
|
|
284
|
+
contract_id = self.get_contract_id(symbol)
|
|
285
|
+
if contract_id is None:
|
|
286
|
+
continue
|
|
287
|
+
self.store.book.id_to_symbol[str(contract_id)] = symbol
|
|
288
|
+
channels.append(f"{depth}:{contract_id}")
|
|
289
|
+
|
|
290
|
+
if not channels:
|
|
291
|
+
raise ValueError("No channels resolved for subscription")
|
|
292
|
+
|
|
293
|
+
payload = {"action": "subscribe", "args": channels}
|
|
294
|
+
# print(payload)
|
|
295
|
+
|
|
296
|
+
ws_app = self.client.ws_connect(
|
|
297
|
+
self.ws_url,
|
|
298
|
+
send_json=payload,
|
|
299
|
+
hdlr_json=self.store.onmessage,
|
|
300
|
+
autoping=False,
|
|
301
|
+
)
|
|
302
|
+
await ws_app._event.wait()
|
|
303
|
+
return ws_app
|
|
304
|
+
|
|
305
|
+
def gen_order_id(self):
|
|
306
|
+
ts = int(time.time() * 1000) # 13位毫秒时间戳
|
|
307
|
+
rand = random.randint(100000, 999999) # 6位随机数
|
|
308
|
+
return int(f"{ts}{rand}")
|
|
309
|
+
|
|
310
|
+
async def place_order(
|
|
311
|
+
self,
|
|
312
|
+
symbol: str,
|
|
313
|
+
*,
|
|
314
|
+
category: Literal[1,2,"limit","market"] = "limit",
|
|
315
|
+
price: float,
|
|
316
|
+
qty: float,
|
|
317
|
+
way: Literal[1, 2, 3, 4, "open_long", "close_short", "close_long", "open_short", "buy", "sell"] = "open_long",
|
|
318
|
+
mode: Literal[1, 2, 3, 4, "gtc", "ioc", "fok", "maker_only", "maker-only", "post_only"] = "gtc",
|
|
319
|
+
open_type: Literal[1, 2, "cross", "isolated"] = "isolated",
|
|
320
|
+
leverage: int | str = 10,
|
|
321
|
+
reverse_vol: int | float = 0,
|
|
322
|
+
trigger_price: float | None = None,
|
|
323
|
+
custom_id: int | str | None = None,
|
|
324
|
+
extra_params: dict[str, Any] | None = None,
|
|
325
|
+
) -> dict[str, Any]:
|
|
326
|
+
"""Submit an order via ``submitOrder``.
|
|
327
|
+
"""
|
|
328
|
+
|
|
329
|
+
contract_id = self.get_contract_id(symbol)
|
|
330
|
+
if contract_id is None:
|
|
331
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
332
|
+
contract_id_int = int(contract_id)
|
|
333
|
+
|
|
334
|
+
detail = self._get_detail_entry(symbol=symbol, market_index=contract_id_int)
|
|
335
|
+
if detail is None:
|
|
336
|
+
await self.update("detail")
|
|
337
|
+
detail = self._get_detail_entry(symbol=symbol, market_index=contract_id_int)
|
|
338
|
+
if detail is None:
|
|
339
|
+
raise ValueError(f"Market metadata unavailable for symbol: {symbol}")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
contract_size_str = detail.get("contract_size") or detail.get("vol_unit") or "1"
|
|
343
|
+
try:
|
|
344
|
+
contract_size_val = float(contract_size_str)
|
|
345
|
+
except (TypeError, ValueError):
|
|
346
|
+
contract_size_val = 1.0
|
|
347
|
+
if contract_size_val <= 0:
|
|
348
|
+
raise ValueError(f"Invalid contract_size for {symbol}: {contract_size_str}")
|
|
349
|
+
|
|
350
|
+
contracts_float = float(qty) / contract_size_val
|
|
351
|
+
contracts_int = int(round(contracts_float))
|
|
352
|
+
if contracts_int <= 0:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"qty too small for contract size ({contract_size_val}): qty={qty}"
|
|
355
|
+
)
|
|
356
|
+
if abs(contracts_float - contracts_int) > 1e-8:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"qty must be a multiple of contract_size ({contract_size_val}). "
|
|
359
|
+
f"Received qty={qty} -> {contracts_float} contracts."
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
price_unit = detail.get("price_unit") or 1
|
|
363
|
+
adjusted_price = float(fmt_value(price, price_unit))
|
|
364
|
+
|
|
365
|
+
category = self._normalize_enum(
|
|
366
|
+
category,
|
|
367
|
+
{
|
|
368
|
+
"limit": 1,
|
|
369
|
+
"market": 2,
|
|
370
|
+
},
|
|
371
|
+
"category",
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
way_value = self._normalize_enum(
|
|
375
|
+
way,
|
|
376
|
+
{
|
|
377
|
+
"open_long": 1,
|
|
378
|
+
"close_short": 2,
|
|
379
|
+
"close_long": 3,
|
|
380
|
+
"open_short": 4,
|
|
381
|
+
"buy": 1,
|
|
382
|
+
"sell": 4,
|
|
383
|
+
},
|
|
384
|
+
"way",
|
|
385
|
+
)
|
|
386
|
+
mode_value = self._normalize_enum(
|
|
387
|
+
mode,
|
|
388
|
+
{
|
|
389
|
+
"gtc": 1,
|
|
390
|
+
"fok": 2,
|
|
391
|
+
"ioc": 3,
|
|
392
|
+
"maker_only": 4,
|
|
393
|
+
"maker-only": 4,
|
|
394
|
+
"post_only": 4,
|
|
395
|
+
},
|
|
396
|
+
"mode",
|
|
397
|
+
)
|
|
398
|
+
open_type_value = self._normalize_enum(
|
|
399
|
+
open_type,
|
|
400
|
+
{
|
|
401
|
+
"cross": 1,
|
|
402
|
+
"isolated": 2,
|
|
403
|
+
},
|
|
404
|
+
"open_type",
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
payload: dict[str, Any] = {
|
|
408
|
+
"place_all_order": False,
|
|
409
|
+
"contract_id": contract_id_int,
|
|
410
|
+
"category": category,
|
|
411
|
+
"price": adjusted_price,
|
|
412
|
+
"vol": contracts_int,
|
|
413
|
+
"way": way_value,
|
|
414
|
+
"mode": mode_value,
|
|
415
|
+
"open_type": open_type_value,
|
|
416
|
+
"leverage": leverage,
|
|
417
|
+
"reverse_vol": reverse_vol,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if trigger_price is not None:
|
|
421
|
+
payload["trigger_price"] = trigger_price
|
|
422
|
+
|
|
423
|
+
payload["custom_id"] = custom_id or self.gen_order_id()
|
|
424
|
+
|
|
425
|
+
if extra_params:
|
|
426
|
+
payload.update(extra_params)
|
|
427
|
+
|
|
428
|
+
# print(payload)
|
|
429
|
+
# exit()
|
|
430
|
+
|
|
431
|
+
res = await self.client.post(
|
|
432
|
+
f"{self.forward_api}/v1/ifcontract/submitOrder",
|
|
433
|
+
json=payload,
|
|
434
|
+
)
|
|
435
|
+
resp = await res.json()
|
|
436
|
+
|
|
437
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
438
|
+
raise ValueError(f"Bitmart submitOrder error: {resp}")
|
|
439
|
+
return resp
|
|
440
|
+
|
|
441
|
+
async def cancel_order(
|
|
442
|
+
self,
|
|
443
|
+
symbol: str,
|
|
444
|
+
order_ids: Sequence[int | str],
|
|
445
|
+
*,
|
|
446
|
+
nonce: int | None = None,
|
|
447
|
+
) -> dict[str, Any]:
|
|
448
|
+
"""Cancel one or multiple orders."""
|
|
449
|
+
|
|
450
|
+
contract_id = self.get_contract_id(symbol)
|
|
451
|
+
if contract_id is None:
|
|
452
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
453
|
+
|
|
454
|
+
payload = {
|
|
455
|
+
"orders": [
|
|
456
|
+
{
|
|
457
|
+
"contract_id": int(contract_id),
|
|
458
|
+
"orders": [int(order_id) for order_id in order_ids],
|
|
459
|
+
}
|
|
460
|
+
],
|
|
461
|
+
"nonce": nonce if nonce is not None else int(time.time()),
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
res = await self.client.post(
|
|
465
|
+
f"{self.forward_api}/v1/ifcontract/cancelOrders",
|
|
466
|
+
json=payload,
|
|
467
|
+
)
|
|
468
|
+
resp = await res.json()
|
|
469
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
470
|
+
raise ValueError(f"Bitmart cancelOrders error: {resp}")
|
|
471
|
+
return resp
|