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,720 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any, Literal, Optional, Sequence
|
|
8
|
+
|
|
9
|
+
import pybotters
|
|
10
|
+
|
|
11
|
+
from .models.bitmart import BitmartDataStore
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Book():
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.limit: int | None = None
|
|
17
|
+
self.store = {}
|
|
18
|
+
|
|
19
|
+
def on_message(self, msg: dict[str, Any], ws=None) -> None:
|
|
20
|
+
data = msg.get("data")
|
|
21
|
+
if not isinstance(data, dict):
|
|
22
|
+
return
|
|
23
|
+
symbol = data.get("symbol")
|
|
24
|
+
self.store[symbol] = data
|
|
25
|
+
|
|
26
|
+
def find(self, query: dict[str, Any]) -> dict[str, Any] | None:
|
|
27
|
+
s = query.get("s")
|
|
28
|
+
S = query.get("S")
|
|
29
|
+
item = self.store.get(s)
|
|
30
|
+
if item:
|
|
31
|
+
if S == "a":
|
|
32
|
+
return [{"s": s, "S": "a", "p": item["asks"][0]['price'], "q": item["asks"][0]['vol']}]
|
|
33
|
+
elif S == "b":
|
|
34
|
+
return [{"s": s, "S": "b", "p": item["bids"][0]['price'], "q": item["bids"][0]['vol']}]
|
|
35
|
+
else:
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
class BitmartDataStore2(BitmartDataStore):
|
|
39
|
+
def _init(self):
|
|
40
|
+
self.bk = Book()
|
|
41
|
+
return super()._init()
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def book(self) -> Book:
|
|
45
|
+
return self.bk
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Bitmart:
|
|
49
|
+
"""Bitmart 合约交易(REST + WebSocket)。"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
client: pybotters.Client,
|
|
54
|
+
*,
|
|
55
|
+
public_api: str | None = None,
|
|
56
|
+
forward_api: str | None = None,
|
|
57
|
+
ws_url: str | None = None,
|
|
58
|
+
account_index: int | None = None,
|
|
59
|
+
apis: str = None
|
|
60
|
+
) -> None:
|
|
61
|
+
self.client = client
|
|
62
|
+
self.store = BitmartDataStore2()
|
|
63
|
+
|
|
64
|
+
self.public_api = public_api or "https://contract-v2.bitmart.com"
|
|
65
|
+
self.private_api = "https://derivatives.bitmart.com"
|
|
66
|
+
self.forward_api = f'{self.private_api}/gw-api/contract-tiger/forward'
|
|
67
|
+
self.ws_url = ws_url or "wss://contract-ws-v2.bitmart.com/v1/ifcontract/realTime"
|
|
68
|
+
self.api_ws_url = "wss://openapi-ws-v2.bitmart.com/api?protocol=1.1"
|
|
69
|
+
self.api_url = "https://api-cloud-v2.bitmart.com"
|
|
70
|
+
self.account_index = account_index
|
|
71
|
+
self.apis = apis
|
|
72
|
+
self.symbol_to_contract_id: dict[str, str] = {}
|
|
73
|
+
self.book = Book()
|
|
74
|
+
|
|
75
|
+
async def __aenter__(self) -> "Bitmart":
|
|
76
|
+
await self.update("detail")
|
|
77
|
+
asyncio.create_task(self.auto_refresh())
|
|
78
|
+
|
|
79
|
+
for entry in self.store.detail.find():
|
|
80
|
+
contract_id = entry.get("contract_id")
|
|
81
|
+
symbol = entry.get("name") or entry.get("display_name")
|
|
82
|
+
if contract_id is None or symbol is None:
|
|
83
|
+
continue
|
|
84
|
+
self.symbol_to_contract_id[str(symbol)] = str(contract_id)
|
|
85
|
+
|
|
86
|
+
return self
|
|
87
|
+
|
|
88
|
+
async def auto_refresh(self, sec=3600, test=False) -> None:
|
|
89
|
+
"""每隔一小时刷新token"""
|
|
90
|
+
client = self.client
|
|
91
|
+
while not client._session.closed:
|
|
92
|
+
|
|
93
|
+
await asyncio.sleep(sec)
|
|
94
|
+
|
|
95
|
+
if client._session.__dict__["_apis"].get("bitmart") is None:
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
# 执行请求
|
|
99
|
+
res = await client.post(
|
|
100
|
+
f"{self.private_api}/gw-api/gateway/token/v2/renew",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
print(await res.text())
|
|
104
|
+
resp:dict = await res.json()
|
|
105
|
+
if resp.get("success") is False:
|
|
106
|
+
raise ValueError(f"Bitmart refreshToken error: {resp}")
|
|
107
|
+
|
|
108
|
+
data:dict = resp.get("data", {})
|
|
109
|
+
new_token = data.get("accessToken")
|
|
110
|
+
secret = data.get("accessSalt")
|
|
111
|
+
|
|
112
|
+
# 加载原来的apis
|
|
113
|
+
apis_dict = client._load_apis(self.apis)
|
|
114
|
+
|
|
115
|
+
device = apis_dict['bitmart'][2]
|
|
116
|
+
|
|
117
|
+
apis_dict["bitmart"] = [new_token, secret, device]
|
|
118
|
+
|
|
119
|
+
client._session.__dict__["_apis"] = client._encode_apis(apis_dict)
|
|
120
|
+
|
|
121
|
+
if test:
|
|
122
|
+
print("Bitmart token refreshed.")
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
|
|
126
|
+
return None
|
|
127
|
+
|
|
128
|
+
def get_contract_id(self, symbol: str) -> str | None:
|
|
129
|
+
"""Resolve contract ID from cached detail data."""
|
|
130
|
+
detail = (
|
|
131
|
+
self.store.detail.get({"name": symbol})
|
|
132
|
+
or self.store.detail.get({"display_name": symbol})
|
|
133
|
+
or self.store.detail.get({"contract_id": symbol})
|
|
134
|
+
)
|
|
135
|
+
if detail is None:
|
|
136
|
+
return None
|
|
137
|
+
contract_id = detail.get("contract_id")
|
|
138
|
+
if contract_id is None:
|
|
139
|
+
return None
|
|
140
|
+
return str(contract_id)
|
|
141
|
+
|
|
142
|
+
def _get_detail_entry(
|
|
143
|
+
self,
|
|
144
|
+
*,
|
|
145
|
+
symbol: str | None = None,
|
|
146
|
+
market_index: int | None = None,
|
|
147
|
+
) -> dict[str, Any] | None:
|
|
148
|
+
if symbol:
|
|
149
|
+
entry = (
|
|
150
|
+
self.store.detail.get({"name": symbol})
|
|
151
|
+
or self.store.detail.get({"display_name": symbol})
|
|
152
|
+
)
|
|
153
|
+
if entry:
|
|
154
|
+
return entry
|
|
155
|
+
|
|
156
|
+
if market_index is not None:
|
|
157
|
+
entries = self.store.detail.find({"contract_id": market_index})
|
|
158
|
+
if entries:
|
|
159
|
+
return entries[0]
|
|
160
|
+
entries = self.store.detail.find({"contract_id": str(market_index)})
|
|
161
|
+
if entries:
|
|
162
|
+
return entries[0]
|
|
163
|
+
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def _normalize_enum(
|
|
168
|
+
value: int | str,
|
|
169
|
+
mapping: dict[str, int],
|
|
170
|
+
field: str,
|
|
171
|
+
) -> int:
|
|
172
|
+
if isinstance(value, str):
|
|
173
|
+
key = value.lower()
|
|
174
|
+
try:
|
|
175
|
+
return mapping[key]
|
|
176
|
+
except KeyError as exc:
|
|
177
|
+
raise ValueError(f"Unsupported {field}: {value}") from exc
|
|
178
|
+
try:
|
|
179
|
+
return int(value)
|
|
180
|
+
except (TypeError, ValueError) as exc:
|
|
181
|
+
raise ValueError(f"Unsupported {field}: {value}") from exc
|
|
182
|
+
|
|
183
|
+
async def update(
|
|
184
|
+
self,
|
|
185
|
+
update_type: Literal[
|
|
186
|
+
"detail",
|
|
187
|
+
"orders",
|
|
188
|
+
"positions",
|
|
189
|
+
"balances",
|
|
190
|
+
"account",
|
|
191
|
+
"all",
|
|
192
|
+
"history_orders",
|
|
193
|
+
"ticker",
|
|
194
|
+
] = "all",
|
|
195
|
+
*,
|
|
196
|
+
orders_params: dict[str, Any] | None = None,
|
|
197
|
+
positions_params: dict[str, Any] | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Refresh cached REST resources."""
|
|
200
|
+
|
|
201
|
+
tasks: dict[str, Any] = {}
|
|
202
|
+
|
|
203
|
+
include_detail = update_type in {"detail", "all"}
|
|
204
|
+
include_orders = update_type in {"orders", "all"}
|
|
205
|
+
include_positions = update_type in {"positions", "all"}
|
|
206
|
+
include_balances = update_type in {"balances", "account", "all"}
|
|
207
|
+
include_history_orders = update_type in {"history_orders"}
|
|
208
|
+
include_ticker = update_type in {"ticker", "all"}
|
|
209
|
+
|
|
210
|
+
if include_detail:
|
|
211
|
+
tasks["detail"] = self.client.get(f"{self.public_api}/v1/ifcontract/contracts_all")
|
|
212
|
+
|
|
213
|
+
if include_orders:
|
|
214
|
+
params = {
|
|
215
|
+
"status": 3,
|
|
216
|
+
"size": 200,
|
|
217
|
+
"orderType": 0,
|
|
218
|
+
"offset": 0,
|
|
219
|
+
"direction": 0,
|
|
220
|
+
"type": 1,
|
|
221
|
+
}
|
|
222
|
+
if orders_params:
|
|
223
|
+
params.update(orders_params)
|
|
224
|
+
tasks["orders"] = self.client.get(
|
|
225
|
+
f"{self.forward_api}/v1/ifcontract/userAllOrders",
|
|
226
|
+
params=params,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if include_positions:
|
|
230
|
+
params = {"status": 1}
|
|
231
|
+
if positions_params:
|
|
232
|
+
params.update(positions_params)
|
|
233
|
+
tasks["positions"] = self.client.get(
|
|
234
|
+
f"{self.forward_api}/v1/ifcontract/userPositions",
|
|
235
|
+
params=params,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if include_balances:
|
|
239
|
+
tasks["balances"] = self.client.get(
|
|
240
|
+
f"{self.forward_api}/v1/ifcontract/copy/trade/user/info",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if include_history_orders:
|
|
244
|
+
d_params = {"offset": 0, "status": 60, "size": 20, "type": 1}
|
|
245
|
+
d_params.update(orders_params or {})
|
|
246
|
+
tasks["history_orders"] = self.client.get(
|
|
247
|
+
f"{self.forward_api}/v1/ifcontract/userAllOrders",
|
|
248
|
+
params=d_params,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
if include_ticker:
|
|
252
|
+
tasks["ticker"] = self.client.get(
|
|
253
|
+
f"{self.public_api}/v1/ifcontract/tickers"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if not tasks:
|
|
257
|
+
raise ValueError(f"Unsupported update_type: {update_type}")
|
|
258
|
+
|
|
259
|
+
results: dict[str, Any] = {}
|
|
260
|
+
for key, req in tasks.items():
|
|
261
|
+
res = await req
|
|
262
|
+
if res.content_type and "json" in res.content_type:
|
|
263
|
+
results[key] = await res.json()
|
|
264
|
+
else:
|
|
265
|
+
text = await res.text()
|
|
266
|
+
try:
|
|
267
|
+
results[key] = json.loads(text)
|
|
268
|
+
except json.JSONDecodeError as exc:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f"Unexpected response format for {key}: {res.content_type} {text[:200]}"
|
|
271
|
+
) from exc
|
|
272
|
+
|
|
273
|
+
if "detail" in results:
|
|
274
|
+
resp = results["detail"]
|
|
275
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
276
|
+
raise ValueError(f"Bitmart detail API error: {resp}")
|
|
277
|
+
self.store.detail._onresponse(resp)
|
|
278
|
+
for entry in self.store.detail.find():
|
|
279
|
+
contract_id = entry.get("contract_id")
|
|
280
|
+
symbol = entry.get("name") or entry.get("display_name")
|
|
281
|
+
if contract_id is None or symbol is None:
|
|
282
|
+
continue
|
|
283
|
+
|
|
284
|
+
if "orders" in results:
|
|
285
|
+
resp = results["orders"]
|
|
286
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
287
|
+
raise ValueError(f"Bitmart orders API error: {resp}")
|
|
288
|
+
self.store.orders._onresponse(resp)
|
|
289
|
+
|
|
290
|
+
if "positions" in results:
|
|
291
|
+
resp = results["positions"]
|
|
292
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
293
|
+
raise ValueError(f"Bitmart positions API error: {resp}")
|
|
294
|
+
self.store.positions._onresponse(resp)
|
|
295
|
+
|
|
296
|
+
if "balances" in results:
|
|
297
|
+
resp = results["balances"]
|
|
298
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
299
|
+
raise ValueError(f"Bitmart balances API error: {resp}")
|
|
300
|
+
self.store.balances._onresponse(resp)
|
|
301
|
+
|
|
302
|
+
if "ticker" in results:
|
|
303
|
+
resp = results["ticker"]
|
|
304
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
305
|
+
raise ValueError(f"Bitmart ticker API error: {resp}")
|
|
306
|
+
self.store.ticker._onresponse(resp)
|
|
307
|
+
|
|
308
|
+
if "history_orders" in results:
|
|
309
|
+
resp = results["history_orders"]
|
|
310
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
311
|
+
raise ValueError(f"Bitmart history_orders API error: {resp}")
|
|
312
|
+
self.store.orders._onresponse(resp)
|
|
313
|
+
|
|
314
|
+
async def sub_orderbook(
|
|
315
|
+
self,
|
|
316
|
+
symbols: Sequence[str] | str,
|
|
317
|
+
*,
|
|
318
|
+
depth_limit: int | None = None,
|
|
319
|
+
) -> pybotters.ws.WebSocketApp:
|
|
320
|
+
"""Subscribe order book channel(s)."""
|
|
321
|
+
|
|
322
|
+
if isinstance(symbols, str):
|
|
323
|
+
symbols = [symbols]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
if not symbols:
|
|
327
|
+
raise ValueError("symbols must not be empty")
|
|
328
|
+
if depth_limit is not None:
|
|
329
|
+
self.store.book.limit = depth_limit
|
|
330
|
+
|
|
331
|
+
hdlr_json = self.store.book.on_message
|
|
332
|
+
|
|
333
|
+
channels: list[str] = []
|
|
334
|
+
for symbol in symbols:
|
|
335
|
+
channels.append(f"futures/depthAll5:{symbol}@100ms")
|
|
336
|
+
|
|
337
|
+
if not channels:
|
|
338
|
+
raise ValueError("No channels resolved for subscription")
|
|
339
|
+
|
|
340
|
+
payload = {"action": "subscribe", "args": channels}
|
|
341
|
+
|
|
342
|
+
ws_app = self.client.ws_connect(
|
|
343
|
+
self.api_ws_url,
|
|
344
|
+
send_json=payload,
|
|
345
|
+
hdlr_json=hdlr_json,
|
|
346
|
+
autoping=False,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
await ws_app._event.wait()
|
|
350
|
+
return ws_app
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def gen_order_id(self):
|
|
354
|
+
ts = int(time.time() * 1000) # 13位毫秒时间戳
|
|
355
|
+
rand = random.randint(100000, 999999) # 6位随机数
|
|
356
|
+
return int(f"{ts}{rand}")
|
|
357
|
+
|
|
358
|
+
async def place_order(
|
|
359
|
+
self,
|
|
360
|
+
symbol: str,
|
|
361
|
+
*,
|
|
362
|
+
category: Literal[1,2,"limit","market"] = "limit",
|
|
363
|
+
price: float,
|
|
364
|
+
qty: Optional[float] = None,
|
|
365
|
+
qty_contract: Optional[int] = None,
|
|
366
|
+
side: Literal[1, 2, 3, 4, "open_long", "close_short", "close_long", "open_short", "buy", "sell"] = "open_long",
|
|
367
|
+
mode: Literal[1, 2, 3, 4, "gtc", "ioc", "fok", "maker_only", "maker-only", "post_only"] = "gtc",
|
|
368
|
+
open_type: Literal[1, 2, "cross", "isolated"] = "isolated",
|
|
369
|
+
leverage: int | str = 10,
|
|
370
|
+
reverse_vol: int | float = 0,
|
|
371
|
+
trigger_price: float | None = None,
|
|
372
|
+
custom_id: int | str | None = None,
|
|
373
|
+
extra_params: dict[str, Any] | None = None,
|
|
374
|
+
use_api: bool = False,
|
|
375
|
+
) -> int:
|
|
376
|
+
"""Submit an order via ``submitOrder``.
|
|
377
|
+
返回值: order_id (int)
|
|
378
|
+
"""
|
|
379
|
+
if qty is None and qty_contract is None:
|
|
380
|
+
raise ValueError("Either qty or qty_contract must be provided.")
|
|
381
|
+
|
|
382
|
+
contract_id = self.get_contract_id(symbol)
|
|
383
|
+
if contract_id is None:
|
|
384
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
385
|
+
contract_id_int = int(contract_id)
|
|
386
|
+
|
|
387
|
+
detail = self._get_detail_entry(symbol=symbol, market_index=contract_id_int)
|
|
388
|
+
if detail is None:
|
|
389
|
+
await self.update("detail")
|
|
390
|
+
detail = self._get_detail_entry(symbol=symbol, market_index=contract_id_int)
|
|
391
|
+
if detail is None:
|
|
392
|
+
raise ValueError(f"Market metadata unavailable for symbol: {symbol}")
|
|
393
|
+
|
|
394
|
+
if qty is not None:
|
|
395
|
+
|
|
396
|
+
contract_size_str = detail.get("contract_size") or detail.get("vol_unit") or "1"
|
|
397
|
+
try:
|
|
398
|
+
contract_size_val = float(contract_size_str)
|
|
399
|
+
except (TypeError, ValueError):
|
|
400
|
+
contract_size_val = 1.0
|
|
401
|
+
if contract_size_val <= 0:
|
|
402
|
+
raise ValueError(f"Invalid contract_size for {symbol}: {contract_size_str}")
|
|
403
|
+
|
|
404
|
+
contracts_float = float(qty) / contract_size_val
|
|
405
|
+
contracts_int = int(round(contracts_float))
|
|
406
|
+
if contracts_int <= 0:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
f"Volume too small for contract size ({contract_size_val}): volume={qty}"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if qty_contract is not None:
|
|
412
|
+
contracts_int = int(qty_contract)
|
|
413
|
+
if contracts_int <= 0:
|
|
414
|
+
raise ValueError(f"Volume must be positive integer contracts: volume={qty_contract}")
|
|
415
|
+
|
|
416
|
+
price_unit = detail.get("price_unit") or 1
|
|
417
|
+
try:
|
|
418
|
+
price_unit_val = float(price_unit)
|
|
419
|
+
except (TypeError, ValueError):
|
|
420
|
+
price_unit_val = 1.0
|
|
421
|
+
if price_unit_val <= 0:
|
|
422
|
+
price_unit_val = 1.0
|
|
423
|
+
|
|
424
|
+
price_value = float(price)
|
|
425
|
+
adjusted_price = int(price_value / price_unit_val) * price_unit_val
|
|
426
|
+
|
|
427
|
+
category = self._normalize_enum(
|
|
428
|
+
category,
|
|
429
|
+
{
|
|
430
|
+
"limit": 1,
|
|
431
|
+
"market": 2,
|
|
432
|
+
},
|
|
433
|
+
"category",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
if category == 2: # market
|
|
437
|
+
adjusted_price = 0.0
|
|
438
|
+
price_fmt = f"{adjusted_price:.15f}".rstrip("0").rstrip(".") or "0"
|
|
439
|
+
|
|
440
|
+
way_value = self._normalize_enum(
|
|
441
|
+
side,
|
|
442
|
+
{
|
|
443
|
+
"open_long": 1,
|
|
444
|
+
"close_short": 2,
|
|
445
|
+
"close_long": 3,
|
|
446
|
+
"open_short": 4,
|
|
447
|
+
"buy": 1,
|
|
448
|
+
"sell": 4,
|
|
449
|
+
},
|
|
450
|
+
"way",
|
|
451
|
+
)
|
|
452
|
+
mode_value = self._normalize_enum(
|
|
453
|
+
mode,
|
|
454
|
+
{
|
|
455
|
+
"gtc": 1,
|
|
456
|
+
"fok": 2,
|
|
457
|
+
"ioc": 3,
|
|
458
|
+
"maker_only": 4,
|
|
459
|
+
"maker-only": 4,
|
|
460
|
+
"post_only": 4,
|
|
461
|
+
},
|
|
462
|
+
"mode",
|
|
463
|
+
)
|
|
464
|
+
open_type_value = self._normalize_enum(
|
|
465
|
+
open_type,
|
|
466
|
+
{
|
|
467
|
+
"cross": 1,
|
|
468
|
+
"isolated": 2,
|
|
469
|
+
},
|
|
470
|
+
"open_type",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
if use_api:
|
|
474
|
+
# Official API path
|
|
475
|
+
order_type_str = "limit" if category == 1 else "market"
|
|
476
|
+
open_type_str = "cross" if open_type_value == 1 else "isolated"
|
|
477
|
+
client_oid = str(custom_id or self.gen_order_id())
|
|
478
|
+
api_payload: dict[str, Any] = {
|
|
479
|
+
"symbol": symbol,
|
|
480
|
+
"client_order_id": client_oid,
|
|
481
|
+
"side": way_value,
|
|
482
|
+
"type": order_type_str,
|
|
483
|
+
"mode": mode_value,
|
|
484
|
+
"leverage": str(leverage),
|
|
485
|
+
"open_type": open_type_str,
|
|
486
|
+
"size": int(contracts_int),
|
|
487
|
+
}
|
|
488
|
+
if order_type_str == "limit":
|
|
489
|
+
api_payload["price"] = price_fmt
|
|
490
|
+
if extra_params:
|
|
491
|
+
api_payload.update(extra_params)
|
|
492
|
+
# Ensure leverage is synchronized via official API before placing order
|
|
493
|
+
try:
|
|
494
|
+
lev_payload = {
|
|
495
|
+
"symbol": symbol,
|
|
496
|
+
"leverage": str(leverage),
|
|
497
|
+
"open_type": open_type_str,
|
|
498
|
+
}
|
|
499
|
+
res_lev = await self.client.post(
|
|
500
|
+
f"{self.api_url}/contract/private/submit-leverage",
|
|
501
|
+
json=lev_payload,
|
|
502
|
+
)
|
|
503
|
+
txt_lev = await res_lev.text()
|
|
504
|
+
try:
|
|
505
|
+
resp_lev = json.loads(txt_lev)
|
|
506
|
+
if resp_lev.get("code") != 1000:
|
|
507
|
+
# ignore and proceed; order may still pass
|
|
508
|
+
pass
|
|
509
|
+
except json.JSONDecodeError:
|
|
510
|
+
pass
|
|
511
|
+
await asyncio.sleep(0.05)
|
|
512
|
+
except Exception:
|
|
513
|
+
pass
|
|
514
|
+
|
|
515
|
+
res = await self.client.post(
|
|
516
|
+
f"{self.api_url}/contract/private/submit-order",
|
|
517
|
+
json=api_payload,
|
|
518
|
+
)
|
|
519
|
+
# Parse response (some errors may return text/plain containing JSON)
|
|
520
|
+
text = await res.text()
|
|
521
|
+
try:
|
|
522
|
+
resp = json.loads(text)
|
|
523
|
+
except json.JSONDecodeError:
|
|
524
|
+
raise ValueError(f"Bitmart API submit-order non-json response: {text[:200]}")
|
|
525
|
+
if resp.get("code") != 1000:
|
|
526
|
+
# Auto-sync leverage once if required, then retry once
|
|
527
|
+
if resp.get("code") in (40012,):
|
|
528
|
+
try:
|
|
529
|
+
# Retry leverage sync via official API then retry the order
|
|
530
|
+
lev_payload = {
|
|
531
|
+
"symbol": symbol,
|
|
532
|
+
"leverage": str(leverage),
|
|
533
|
+
"open_type": open_type_str,
|
|
534
|
+
}
|
|
535
|
+
await self.client.post(
|
|
536
|
+
f"{self.api_url}/contract/private/submit-leverage",
|
|
537
|
+
json=lev_payload,
|
|
538
|
+
)
|
|
539
|
+
await asyncio.sleep(0.05)
|
|
540
|
+
res2 = await self.client.post(
|
|
541
|
+
f"{self.api_url}/contract/private/submit-order",
|
|
542
|
+
json=api_payload,
|
|
543
|
+
)
|
|
544
|
+
text2 = await res2.text()
|
|
545
|
+
try:
|
|
546
|
+
resp2 = json.loads(text2)
|
|
547
|
+
except json.JSONDecodeError:
|
|
548
|
+
raise ValueError(
|
|
549
|
+
f"Bitmart API submit-order non-json response: {text2[:200]}"
|
|
550
|
+
)
|
|
551
|
+
if resp2.get("code") == 1000:
|
|
552
|
+
return resp2.get("data", {}).get("order_id")
|
|
553
|
+
else:
|
|
554
|
+
raise ValueError(f"Bitmart API submit-order error: {resp2}")
|
|
555
|
+
except Exception:
|
|
556
|
+
# Fall through to raise original error if sync failed
|
|
557
|
+
pass
|
|
558
|
+
raise ValueError(f"Bitmart API submit-order error: {resp}")
|
|
559
|
+
return resp.get("data", {}).get("order_id")
|
|
560
|
+
else:
|
|
561
|
+
payload: dict[str, Any] = {
|
|
562
|
+
"place_all_order": False,
|
|
563
|
+
"contract_id": contract_id_int,
|
|
564
|
+
"category": category,
|
|
565
|
+
"price": price_fmt,
|
|
566
|
+
"vol": contracts_int,
|
|
567
|
+
"way": way_value,
|
|
568
|
+
"mode": mode_value,
|
|
569
|
+
"open_type": open_type_value,
|
|
570
|
+
"leverage": leverage,
|
|
571
|
+
"reverse_vol": reverse_vol,
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if trigger_price is not None:
|
|
575
|
+
payload["trigger_price"] = trigger_price
|
|
576
|
+
|
|
577
|
+
payload["custom_id"] = custom_id or self.gen_order_id()
|
|
578
|
+
|
|
579
|
+
if extra_params:
|
|
580
|
+
payload.update(extra_params)
|
|
581
|
+
|
|
582
|
+
res = await self.client.post(
|
|
583
|
+
f"{self.forward_api}/v1/ifcontract/submitOrder",
|
|
584
|
+
json=payload,
|
|
585
|
+
)
|
|
586
|
+
resp = await res.json()
|
|
587
|
+
|
|
588
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
589
|
+
raise ValueError(f"Bitmart submitOrder error: {resp}")
|
|
590
|
+
return resp.get("data", {}).get("order_id")
|
|
591
|
+
|
|
592
|
+
async def cancel_order(
|
|
593
|
+
self,
|
|
594
|
+
symbol: str,
|
|
595
|
+
order_ids: Sequence[int | str],
|
|
596
|
+
*,
|
|
597
|
+
nonce: int | None = None,
|
|
598
|
+
) -> dict[str, Any]:
|
|
599
|
+
"""Cancel one or multiple orders."""
|
|
600
|
+
|
|
601
|
+
contract_id = self.get_contract_id(symbol)
|
|
602
|
+
if contract_id is None:
|
|
603
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
604
|
+
|
|
605
|
+
payload = {
|
|
606
|
+
"orders": [
|
|
607
|
+
{
|
|
608
|
+
"contract_id": int(contract_id),
|
|
609
|
+
"orders": [int(order_id) for order_id in order_ids],
|
|
610
|
+
}
|
|
611
|
+
],
|
|
612
|
+
"nonce": nonce if nonce is not None else int(time.time()),
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
res = await self.client.post(
|
|
616
|
+
f"{self.forward_api}/v1/ifcontract/cancelOrders",
|
|
617
|
+
json=payload,
|
|
618
|
+
)
|
|
619
|
+
resp = await res.json()
|
|
620
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
621
|
+
raise ValueError(f"Bitmart cancelOrders error: {resp}")
|
|
622
|
+
return resp
|
|
623
|
+
|
|
624
|
+
async def get_leverage(
|
|
625
|
+
self,
|
|
626
|
+
*,
|
|
627
|
+
symbol: str | None = None,
|
|
628
|
+
contract_id: int | str | None = None,
|
|
629
|
+
) -> dict[str, Any]:
|
|
630
|
+
"""
|
|
631
|
+
获取指定合约的杠杆信息(可通过 contract_id 或 symbol 查询)。
|
|
632
|
+
|
|
633
|
+
参数:
|
|
634
|
+
symbol (str | None): 合约符号,例如 "BTCUSDT"。如果未传入 contract_id,则会自动解析。
|
|
635
|
+
contract_id (int | str | None): 合约 ID,可直接指定。
|
|
636
|
+
|
|
637
|
+
返回:
|
|
638
|
+
dict[str, Any]: 杠杆信息字典,典型返回结构如下:
|
|
639
|
+
{
|
|
640
|
+
"contract_id": 1,
|
|
641
|
+
"leverage": 96, # 当前杠杆倍数
|
|
642
|
+
"open_type": 2, # 开仓类型 (1=全仓, 2=逐仓)
|
|
643
|
+
"max_leverage": {
|
|
644
|
+
"contract_id": 1,
|
|
645
|
+
"leverage": "200", # 最大可用杠杆倍数
|
|
646
|
+
"open_type": 0,
|
|
647
|
+
"imr": "0.005", # 初始保证金率
|
|
648
|
+
"mmr": "0.0025", # 维持保证金率
|
|
649
|
+
"value": "0"
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
异常:
|
|
654
|
+
ValueError: 当未提供 symbol 或 contract_id,或接口返回错误时抛出。
|
|
655
|
+
|
|
656
|
+
示例:
|
|
657
|
+
data = await bitmart.get_leverage(symbol="BTCUSDT")
|
|
658
|
+
print(data["leverage"]) # 输出当前杠杆倍数
|
|
659
|
+
"""
|
|
660
|
+
if contract_id is None:
|
|
661
|
+
if symbol is not None:
|
|
662
|
+
contract_id = self.get_contract_id(symbol)
|
|
663
|
+
if contract_id is None:
|
|
664
|
+
raise ValueError("Either contract_id or a valid symbol must be provided to get leverage info.")
|
|
665
|
+
res = await self.client.get(
|
|
666
|
+
f"{self.forward_api}/v1/ifcontract/getLeverage",
|
|
667
|
+
params={"contract_id": contract_id},
|
|
668
|
+
)
|
|
669
|
+
resp = await res.json()
|
|
670
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
671
|
+
raise ValueError(f"Bitmart getLeverage error: {resp}")
|
|
672
|
+
return resp.get("data")
|
|
673
|
+
|
|
674
|
+
async def bind_leverage(
|
|
675
|
+
self,
|
|
676
|
+
*,
|
|
677
|
+
symbol: str | None = None,
|
|
678
|
+
contract_id: int | str | None = None,
|
|
679
|
+
leverage: int | str,
|
|
680
|
+
open_type: Literal[1, 2] = 2,
|
|
681
|
+
) -> None:
|
|
682
|
+
"""
|
|
683
|
+
绑定(设置)指定合约的杠杆倍数。
|
|
684
|
+
|
|
685
|
+
参数:
|
|
686
|
+
symbol (str | None): 合约符号,例如 "BTCUSDT"。若未传入 contract_id,会自动解析。
|
|
687
|
+
contract_id (int | str | None): 合约 ID,可直接指定。
|
|
688
|
+
leverage (int | str): 要设置的杠杆倍数,如 20、50、100。
|
|
689
|
+
open_type (int): 开仓模式,1=全仓(Cross),2=逐仓(Isolated)。
|
|
690
|
+
|
|
691
|
+
返回:
|
|
692
|
+
None — 如果接口调用成功,不返回任何内容。
|
|
693
|
+
若失败则抛出 ValueError。
|
|
694
|
+
|
|
695
|
+
异常:
|
|
696
|
+
ValueError: 当未提供 symbol 或 contract_id,或接口返回错误时抛出。
|
|
697
|
+
|
|
698
|
+
示例:
|
|
699
|
+
await bitmart.bind_leverage(symbol="BTCUSDT", leverage=50, open_type=2)
|
|
700
|
+
"""
|
|
701
|
+
if contract_id is None:
|
|
702
|
+
if symbol is not None:
|
|
703
|
+
contract_id = self.get_contract_id(symbol)
|
|
704
|
+
if contract_id is None:
|
|
705
|
+
raise ValueError("Either contract_id or a valid symbol must be provided to bind leverage.")
|
|
706
|
+
|
|
707
|
+
payload = {
|
|
708
|
+
"contract_id": int(contract_id),
|
|
709
|
+
"leverage": leverage,
|
|
710
|
+
"open_type": open_type,
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
res = await self.client.post(
|
|
714
|
+
f"{self.forward_api}/v1/ifcontract/bindLeverage",
|
|
715
|
+
json=payload,
|
|
716
|
+
)
|
|
717
|
+
resp = await res.json()
|
|
718
|
+
if resp.get("success") is False or resp.get("errno") not in (None, "OK"):
|
|
719
|
+
raise ValueError(f"Bitmart bindLeverage error: {resp}")
|
|
720
|
+
return None
|