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,550 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import Literal, Optional
|
|
3
|
+
import pybotters
|
|
4
|
+
from .models.ourbit import OurbitSwapDataStore, OurbitSpotDataStore
|
|
5
|
+
from decimal import Decimal, ROUND_HALF_UP
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class OurbitSwap:
|
|
9
|
+
|
|
10
|
+
def __init__(self, client: pybotters.Client):
|
|
11
|
+
"""
|
|
12
|
+
✅ 完成:
|
|
13
|
+
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
self.client = client
|
|
17
|
+
self.store = OurbitSwapDataStore()
|
|
18
|
+
self.api_url = "https://futures.ourbit.com"
|
|
19
|
+
self.ws_url = "wss://futures.ourbit.com/edge"
|
|
20
|
+
|
|
21
|
+
async def __aenter__(self) -> "OurbitSwap":
|
|
22
|
+
client = self.client
|
|
23
|
+
await self.store.initialize(
|
|
24
|
+
client.get(f"{self.api_url}/api/v1/contract/detailV2?client=web")
|
|
25
|
+
)
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
async def update(
|
|
29
|
+
self,
|
|
30
|
+
update_type: Literal["position", "orders", "balance", "ticker", "all"] = "all",
|
|
31
|
+
):
|
|
32
|
+
"""由于交易所很多不支持ws推送,这里使用Rest"""
|
|
33
|
+
all_urls = [
|
|
34
|
+
f"{self.api_url}/api/v1/private/position/open_positions",
|
|
35
|
+
f"{self.api_url}/api/v1/private/order/list/open_orders?page_size=200",
|
|
36
|
+
f"{self.api_url}/api/v1/private/account/assets",
|
|
37
|
+
f"{self.api_url}/api/v1/contract/ticker",
|
|
38
|
+
f"{self.api_url}/api/platform/spot/market/v2/symbols",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
url_map = {
|
|
42
|
+
"position": [all_urls[0]],
|
|
43
|
+
"orders": [all_urls[1]],
|
|
44
|
+
"balance": [all_urls[2]],
|
|
45
|
+
"ticker": [all_urls[3]],
|
|
46
|
+
"all": all_urls,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
urls = url_map[update_type]
|
|
51
|
+
except KeyError:
|
|
52
|
+
raise ValueError(f"update_type err: {update_type}")
|
|
53
|
+
|
|
54
|
+
# 直接传协程进去,initialize 会自己 await
|
|
55
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
|
56
|
+
|
|
57
|
+
async def sub_tickers(self):
|
|
58
|
+
self.client.ws_connect(
|
|
59
|
+
self.ws_url,
|
|
60
|
+
send_json={"method": "sub.tickers", "param": {"timezone": "UTC+8"}},
|
|
61
|
+
hdlr_json=self.store.onmessage,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
|
65
|
+
if isinstance(symbols, str):
|
|
66
|
+
symbols = [symbols]
|
|
67
|
+
|
|
68
|
+
send_jsons = []
|
|
69
|
+
# send_json={"method":"sub.depth.step","param":{"symbol":"BTC_USDT","step":"0.1"}},
|
|
70
|
+
|
|
71
|
+
for symbol in symbols:
|
|
72
|
+
step = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
|
73
|
+
step_str = str(Decimal(str(step)).normalize())
|
|
74
|
+
|
|
75
|
+
send_jsons.append(
|
|
76
|
+
{
|
|
77
|
+
"method": "sub.depth.step",
|
|
78
|
+
"param": {
|
|
79
|
+
"symbol": symbol,
|
|
80
|
+
"step": step_str,
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
await self.client.ws_connect(
|
|
86
|
+
self.ws_url, send_json=send_jsons, hdlr_json=self.store.onmessage
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
async def sub_personal(self):
|
|
90
|
+
wsapp = self.client.ws_connect(
|
|
91
|
+
self.ws_url,
|
|
92
|
+
send_json={"method": "sub.personal.user.preference"},
|
|
93
|
+
hdlr_json=self.store.onmessage,
|
|
94
|
+
)
|
|
95
|
+
await wsapp._event.wait()
|
|
96
|
+
|
|
97
|
+
def ret_content(self, res: pybotters.FetchResult):
|
|
98
|
+
match res.data:
|
|
99
|
+
case {"success": True}:
|
|
100
|
+
return res.data["data"]
|
|
101
|
+
case _:
|
|
102
|
+
raise Exception(f"Failed api {res.response.url}: {res.data}")
|
|
103
|
+
|
|
104
|
+
def fmt_price(self, symbol, price: float) -> float:
|
|
105
|
+
tick = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
|
106
|
+
tick_dec = Decimal(str(tick))
|
|
107
|
+
price_dec = Decimal(str(price))
|
|
108
|
+
return float(
|
|
109
|
+
(price_dec / tick_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP)
|
|
110
|
+
* tick_dec
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def place_order(
|
|
114
|
+
self,
|
|
115
|
+
symbol: str,
|
|
116
|
+
side: Literal["buy", "sell", "close_buy", "close_sell"],
|
|
117
|
+
size: float = None,
|
|
118
|
+
price: float = None,
|
|
119
|
+
order_type: Literal["market", "limit_GTC", "limit_IOC"] = "market",
|
|
120
|
+
usdt_amount: Optional[float] = None,
|
|
121
|
+
leverage: Optional[int] = 20,
|
|
122
|
+
position_id: Optional[int] = None,
|
|
123
|
+
quantity: float = None, # 兼容参数,不使用
|
|
124
|
+
):
|
|
125
|
+
"""
|
|
126
|
+
size为合约张数, openType 1 为逐仓, 2为全仓
|
|
127
|
+
|
|
128
|
+
.. code ::
|
|
129
|
+
{
|
|
130
|
+
"orderId": "219602019841167810",
|
|
131
|
+
"ts": 1756395601543
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
if (size is None) == (usdt_amount is None):
|
|
136
|
+
raise ValueError("params err")
|
|
137
|
+
|
|
138
|
+
max_lev = self.store.detail.find({"symbol": symbol})[0].get("max_lev")
|
|
139
|
+
|
|
140
|
+
if usdt_amount is not None:
|
|
141
|
+
cs = self.store.detail.find({"symbol": symbol})[0].get("contract_sz")
|
|
142
|
+
size = max(int(usdt_amount / cs / price), 1)
|
|
143
|
+
|
|
144
|
+
if price is not None:
|
|
145
|
+
price = self.fmt_price(symbol, price)
|
|
146
|
+
|
|
147
|
+
leverage = min(max_lev, leverage)
|
|
148
|
+
|
|
149
|
+
data = {
|
|
150
|
+
"symbol": symbol,
|
|
151
|
+
"side": 1 if side == "buy" else 3,
|
|
152
|
+
"openType": 2,
|
|
153
|
+
"type": "5",
|
|
154
|
+
"vol": size,
|
|
155
|
+
"leverage": leverage,
|
|
156
|
+
"marketCeiling": False,
|
|
157
|
+
"priceProtect": "0",
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if order_type == "limit_IOC":
|
|
161
|
+
data["type"] = 3
|
|
162
|
+
data["price"] = str(price)
|
|
163
|
+
if order_type == "limit_GTC":
|
|
164
|
+
data["type"] = "1"
|
|
165
|
+
data["price"] = str(price)
|
|
166
|
+
|
|
167
|
+
if "close" in side:
|
|
168
|
+
if side == "close_buy":
|
|
169
|
+
data["side"] = 2
|
|
170
|
+
elif side == "close_sell":
|
|
171
|
+
data["side"] = 4
|
|
172
|
+
|
|
173
|
+
if position_id is None:
|
|
174
|
+
raise ValueError("position_id is required for closing position")
|
|
175
|
+
data["positionId"] = position_id
|
|
176
|
+
|
|
177
|
+
res = await self.client.fetch(
|
|
178
|
+
"POST", f"{self.api_url}/api/v1/private/order/create", data=data
|
|
179
|
+
)
|
|
180
|
+
# 'orderId' =
|
|
181
|
+
# '226474723700166962'
|
|
182
|
+
# 'ts' =
|
|
183
|
+
# 1758034181833
|
|
184
|
+
ret_c = self.ret_content(res)
|
|
185
|
+
# 只返回 orderId
|
|
186
|
+
return ret_c["orderId"]
|
|
187
|
+
|
|
188
|
+
async def place_tpsl(
|
|
189
|
+
self,
|
|
190
|
+
position_id: int,
|
|
191
|
+
take_profit: Optional[float] = None,
|
|
192
|
+
stop_loss: Optional[float] = None,
|
|
193
|
+
):
|
|
194
|
+
"""
|
|
195
|
+
position_id 持仓ID
|
|
196
|
+
|
|
197
|
+
.. code:: json
|
|
198
|
+
|
|
199
|
+
{
|
|
200
|
+
"success": true,
|
|
201
|
+
"code": 0,
|
|
202
|
+
"data": 2280508
|
|
203
|
+
}
|
|
204
|
+
"""
|
|
205
|
+
if (take_profit is None) and (stop_loss is None):
|
|
206
|
+
raise ValueError("params err")
|
|
207
|
+
|
|
208
|
+
data = {
|
|
209
|
+
"positionId": position_id,
|
|
210
|
+
"profitTrend": "1",
|
|
211
|
+
"lossTrend": "1",
|
|
212
|
+
"profitLossVolType": "SAME",
|
|
213
|
+
"volType": 2,
|
|
214
|
+
"takeProfitReverse": 2,
|
|
215
|
+
"stopLossReverse": 2,
|
|
216
|
+
"priceProtect": "0",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if take_profit is not None:
|
|
220
|
+
data["takeProfitPrice"] = take_profit
|
|
221
|
+
if stop_loss is not None:
|
|
222
|
+
data["stopLossPrice"] = stop_loss
|
|
223
|
+
|
|
224
|
+
res = await self.client.fetch(
|
|
225
|
+
"POST", f"{self.api_url}/api/v1/private/stoporder/place", data=data
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
return self.ret_content(res)
|
|
229
|
+
|
|
230
|
+
async def cancel_orders(self, order_ids: list[str]):
|
|
231
|
+
res = await self.client.fetch(
|
|
232
|
+
"POST",
|
|
233
|
+
f"{self.api_url}/api/v1/private/order/cancel",
|
|
234
|
+
data=order_ids,
|
|
235
|
+
)
|
|
236
|
+
return self.ret_content(res)
|
|
237
|
+
|
|
238
|
+
async def query_orders(
|
|
239
|
+
self,
|
|
240
|
+
symbol: str,
|
|
241
|
+
states: list[Literal["filled", "canceled"]], # filled:已成交, canceled:已撤销
|
|
242
|
+
start_time: Optional[int] = None,
|
|
243
|
+
end_time: Optional[int] = None,
|
|
244
|
+
page_size: int = 200,
|
|
245
|
+
page_num: int = 1,
|
|
246
|
+
):
|
|
247
|
+
"""查询历史订单
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
symbol: 交易对
|
|
251
|
+
states: 订单状态列表 ["filled":已成交, "canceled":已撤销]
|
|
252
|
+
start_time: 开始时间戳(毫秒), 可选
|
|
253
|
+
end_time: 结束时间戳(毫秒), 可选
|
|
254
|
+
page_size: 每页数量, 默认200
|
|
255
|
+
page_num: 页码, 默认1
|
|
256
|
+
"""
|
|
257
|
+
state_map = {"filled": 3, "canceled": 4}
|
|
258
|
+
|
|
259
|
+
params = {
|
|
260
|
+
"symbol": symbol,
|
|
261
|
+
"states": ",".join(str(state_map[state]) for state in states),
|
|
262
|
+
"page_size": page_size,
|
|
263
|
+
"page_num": page_num,
|
|
264
|
+
"category": 1,
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if start_time:
|
|
268
|
+
params["start_time"] = start_time
|
|
269
|
+
if end_time:
|
|
270
|
+
params["end_time"] = end_time
|
|
271
|
+
|
|
272
|
+
res = await self.client.fetch(
|
|
273
|
+
"GET",
|
|
274
|
+
f"{self.api_url}/api/v1/private/order/list/history_orders",
|
|
275
|
+
params=params,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
return self.ret_content(res)
|
|
279
|
+
|
|
280
|
+
async def query_order(self, order_id: str):
|
|
281
|
+
"""查询单个订单的详细信息
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
order_id: 订单ID
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
..code:python
|
|
288
|
+
|
|
289
|
+
订单详情数据,例如:
|
|
290
|
+
[
|
|
291
|
+
{
|
|
292
|
+
"id": "38600506", # 成交ID
|
|
293
|
+
"symbol": "SOL_USDT", # 交易对
|
|
294
|
+
"side": 4, # 方向(1:买入, 3:卖出, 4:平仓)
|
|
295
|
+
"vol": 1, # 成交数量
|
|
296
|
+
"price": 204.11, # 成交价格
|
|
297
|
+
"fee": 0.00081644, # 手续费
|
|
298
|
+
"feeCurrency": "USDT", # 手续费币种
|
|
299
|
+
"profit": -0.0034, # 盈亏
|
|
300
|
+
"category": 1, # 品类
|
|
301
|
+
"orderId": "219079365441409152", # 订单ID
|
|
302
|
+
"timestamp": 1756270991000, # 时间戳
|
|
303
|
+
"positionMode": 1, # 持仓模式
|
|
304
|
+
"voucher": false, # 是否使用代金券
|
|
305
|
+
"taker": true # 是否是taker
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
"""
|
|
309
|
+
res = await self.client.fetch(
|
|
310
|
+
"GET",
|
|
311
|
+
f"{self.api_url}/api/v1/private/order/deal_details/{order_id}",
|
|
312
|
+
)
|
|
313
|
+
return self.ret_content(res)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class OurbitSpot:
|
|
317
|
+
|
|
318
|
+
def __init__(self, client: pybotters.Client, personal_msg_cb: callable=None):
|
|
319
|
+
"""
|
|
320
|
+
✅ 完成:
|
|
321
|
+
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
self.client = client
|
|
325
|
+
self.store = OurbitSpotDataStore()
|
|
326
|
+
self.api_url = "https://www.ourbit.com"
|
|
327
|
+
self.ws_url = "wss://www.ourbit.com/ws"
|
|
328
|
+
self.personal_msg_cb = personal_msg_cb
|
|
329
|
+
|
|
330
|
+
async def __aenter__(self) -> "OurbitSpot":
|
|
331
|
+
client = self.client
|
|
332
|
+
await self.store.initialize(
|
|
333
|
+
client.get(f"{self.api_url}/api/platform/spot/market/v2/symbols")
|
|
334
|
+
)
|
|
335
|
+
return self
|
|
336
|
+
|
|
337
|
+
async def update(
|
|
338
|
+
self, update_type: Literal["orders", "balance", "ticker", "book", "all"] = "all"
|
|
339
|
+
):
|
|
340
|
+
|
|
341
|
+
all_urls = [
|
|
342
|
+
f"{self.api_url}/api/platform/spot/order/current/orders/v2?orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3",
|
|
343
|
+
f"{self.api_url}/api/assetbussiness/asset/spot/statistic",
|
|
344
|
+
f"{self.api_url}/api/platform/spot/market/v2/tickers",
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
# orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3
|
|
348
|
+
|
|
349
|
+
url_map = {
|
|
350
|
+
"orders": [all_urls[0]],
|
|
351
|
+
"balance": [all_urls[1]],
|
|
352
|
+
"ticker": [all_urls[2]],
|
|
353
|
+
"all": all_urls,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try:
|
|
357
|
+
urls = url_map[update_type]
|
|
358
|
+
except KeyError:
|
|
359
|
+
raise ValueError(f"Unknown update type: {update_type}")
|
|
360
|
+
|
|
361
|
+
# 直接传协程进去,initialize 会自己 await
|
|
362
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
|
363
|
+
|
|
364
|
+
async def sub_personal(self):
|
|
365
|
+
"""订阅个人频道"""
|
|
366
|
+
# https://www.ourbit.com/ucenter/api/ws_token
|
|
367
|
+
res = await self.client.fetch("GET", f"{self.api_url}/ucenter/api/ws_token")
|
|
368
|
+
|
|
369
|
+
token = res.data["data"].get("wsToken")
|
|
370
|
+
|
|
371
|
+
app = self.client.ws_connect(
|
|
372
|
+
f"{self.ws_url}?wsToken={token}&platform=web",
|
|
373
|
+
send_json={
|
|
374
|
+
"method": "SUBSCRIPTION",
|
|
375
|
+
"params": [
|
|
376
|
+
"spot@private.orders",
|
|
377
|
+
"spot@private.trigger.orders",
|
|
378
|
+
"spot@private.balances",
|
|
379
|
+
],
|
|
380
|
+
"id": 1,
|
|
381
|
+
},
|
|
382
|
+
hdlr_json=(
|
|
383
|
+
self.store.onmessage
|
|
384
|
+
if self.personal_msg_cb is None
|
|
385
|
+
else [self.store.onmessage, self.personal_msg_cb]
|
|
386
|
+
),
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
await app._event.wait()
|
|
390
|
+
|
|
391
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
|
392
|
+
"""订阅订单簿深度数据
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
symbols: 交易对符号,可以是单个字符串或字符串列表
|
|
396
|
+
"""
|
|
397
|
+
import logging
|
|
398
|
+
|
|
399
|
+
logger = logging.getLogger("OurbitSpot")
|
|
400
|
+
|
|
401
|
+
if isinstance(symbols, str):
|
|
402
|
+
symbols = [symbols]
|
|
403
|
+
|
|
404
|
+
# 构建订阅参数
|
|
405
|
+
subscription_params = []
|
|
406
|
+
for symbol in symbols:
|
|
407
|
+
subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
|
|
408
|
+
|
|
409
|
+
# 一次sub20个,超过需要分开订阅
|
|
410
|
+
for i in range(0, len(subscription_params), 20):
|
|
411
|
+
wsapp = self.client.ws_connect(
|
|
412
|
+
"wss://www.ourbit.com/ws?platform=web",
|
|
413
|
+
send_json={
|
|
414
|
+
"method": "SUBSCRIPTION",
|
|
415
|
+
"params": subscription_params[i : i + 20],
|
|
416
|
+
"id": 2,
|
|
417
|
+
},
|
|
418
|
+
hdlr_json=self.store.onmessage,
|
|
419
|
+
)
|
|
420
|
+
await wsapp._event.wait()
|
|
421
|
+
|
|
422
|
+
# await asyncio.sleep(1) # 等待ws连接稳定
|
|
423
|
+
|
|
424
|
+
# 并发获取每个交易对的初始深度数据
|
|
425
|
+
tasks = [
|
|
426
|
+
self.client.fetch(
|
|
427
|
+
"GET", f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}"
|
|
428
|
+
)
|
|
429
|
+
for symbol in symbols
|
|
430
|
+
]
|
|
431
|
+
|
|
432
|
+
# 等待所有请求完成
|
|
433
|
+
responses = await asyncio.gather(*tasks)
|
|
434
|
+
|
|
435
|
+
# 处理响应数据
|
|
436
|
+
for idx, response in enumerate(responses):
|
|
437
|
+
symbol = symbols[idx]
|
|
438
|
+
self.store.book._onresponse(response.data)
|
|
439
|
+
|
|
440
|
+
async def check_loss():
|
|
441
|
+
await asyncio.sleep(1)
|
|
442
|
+
while True:
|
|
443
|
+
loss = self.store.book.loss
|
|
444
|
+
for symbol, is_loss in loss.items():
|
|
445
|
+
if is_loss:
|
|
446
|
+
resp = await self.client.fetch(
|
|
447
|
+
"GET",
|
|
448
|
+
f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}",
|
|
449
|
+
)
|
|
450
|
+
self.store.book._onresponse(resp.data)
|
|
451
|
+
await asyncio.sleep(1)
|
|
452
|
+
|
|
453
|
+
asyncio.create_task(check_loss())
|
|
454
|
+
|
|
455
|
+
async def place_order(
|
|
456
|
+
self,
|
|
457
|
+
symbol: str,
|
|
458
|
+
side: Literal["buy", "sell"],
|
|
459
|
+
price: float = None,
|
|
460
|
+
quantity: float = None,
|
|
461
|
+
order_type: Literal["market", "limit"] = "limit",
|
|
462
|
+
usdt_amount: float = None,
|
|
463
|
+
):
|
|
464
|
+
"""现货下单
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
symbol: 交易对,如 "SOL_USDT"
|
|
468
|
+
side: 买卖方向 "buy" 或 "sell"
|
|
469
|
+
price: 价格,市价单可为None
|
|
470
|
+
quantity: 数量
|
|
471
|
+
order_type: 订单类型 "market" 或 "limit"
|
|
472
|
+
usdt_amount: USDT金额,如果指定则根据价格计算数量
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
订单响应数据
|
|
476
|
+
"""
|
|
477
|
+
# 参数检查
|
|
478
|
+
if order_type == "limit" and price is None:
|
|
479
|
+
raise ValueError("Limit orders require a price")
|
|
480
|
+
if quantity is None and usdt_amount is None:
|
|
481
|
+
raise ValueError("Either quantity or usdt_amount must be specified")
|
|
482
|
+
|
|
483
|
+
# 解析交易对
|
|
484
|
+
parts = symbol.split("_")
|
|
485
|
+
if len(parts) != 2:
|
|
486
|
+
raise ValueError(f"Invalid symbol format: {symbol}")
|
|
487
|
+
|
|
488
|
+
currency, market = parts
|
|
489
|
+
|
|
490
|
+
# 获取交易对详情
|
|
491
|
+
detail = self.store.detail.get({"name": currency})
|
|
492
|
+
if not detail:
|
|
493
|
+
raise ValueError(f"Unknown currency: {currency}")
|
|
494
|
+
|
|
495
|
+
price_scale = detail.get("price_scale")
|
|
496
|
+
quantity_scale = detail.get("quantity_scale")
|
|
497
|
+
|
|
498
|
+
# 构建请求数据
|
|
499
|
+
data = {"currency": currency, "market": market, "tradeType": side.upper()}
|
|
500
|
+
|
|
501
|
+
# 处理市价单和限价单的不同参数
|
|
502
|
+
if order_type == "limit":
|
|
503
|
+
data["orderType"] = "LIMIT_ORDER"
|
|
504
|
+
data["price"] = str(
|
|
505
|
+
round(price, price_scale) if price_scale is not None else price
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
# 计算并设置数量
|
|
509
|
+
if quantity is None and usdt_amount is not None and price:
|
|
510
|
+
quantity = usdt_amount / price
|
|
511
|
+
|
|
512
|
+
if quantity_scale is not None:
|
|
513
|
+
quantity = round(quantity, quantity_scale)
|
|
514
|
+
data["quantity"] = str(quantity)
|
|
515
|
+
|
|
516
|
+
elif order_type == "market":
|
|
517
|
+
data["orderType"] = "MARKET_ORDER"
|
|
518
|
+
|
|
519
|
+
# 市价单可以使用数量或金额,但不能同时使用
|
|
520
|
+
if usdt_amount is not None:
|
|
521
|
+
data["amount"] = str(usdt_amount)
|
|
522
|
+
else:
|
|
523
|
+
if quantity_scale is not None:
|
|
524
|
+
quantity = round(quantity, quantity_scale)
|
|
525
|
+
data["quantity"] = str(quantity)
|
|
526
|
+
|
|
527
|
+
if price:
|
|
528
|
+
data["price"] = str(price)
|
|
529
|
+
|
|
530
|
+
# 确定API端点
|
|
531
|
+
url = f'{self.api_url}/api/platform/spot/{"v4/order/place" if order_type == "market" else "order/place"}'
|
|
532
|
+
# print(f"Placing {symbol}: {data}")
|
|
533
|
+
# 发送请求
|
|
534
|
+
res = await self.client.fetch("POST", url, json=data)
|
|
535
|
+
|
|
536
|
+
# 处理响应
|
|
537
|
+
if res.data.get("msg") == "success":
|
|
538
|
+
return res.data["data"]
|
|
539
|
+
raise Exception(f"Failed to place order: {res.data}")
|
|
540
|
+
|
|
541
|
+
async def cancel_orders(self, order_ids: list[str]):
|
|
542
|
+
|
|
543
|
+
for order_id in order_ids:
|
|
544
|
+
url = f"{self.api_url}/api/platform/spot/order/cancel/v2?orderId={order_id}"
|
|
545
|
+
await self.client.fetch("DELETE", url)
|
|
546
|
+
|
|
547
|
+
async def cancel_order(self, order_id: str):
|
|
548
|
+
|
|
549
|
+
url = f"{self.api_url}/api/platform/spot/order/cancel/v2?orderId={order_id}"
|
|
550
|
+
res = await self.client.fetch("DELETE", url)
|