hyperquant 0.3__py3-none-any.whl → 0.4__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 +47 -10
- hyperquant/broker/hyperliquid.py +0 -2
- hyperquant/broker/models/ourbit.py +635 -83
- hyperquant/broker/ourbit.py +277 -11
- hyperquant/broker/ws.py +37 -1
- {hyperquant-0.3.dist-info → hyperquant-0.4.dist-info}/METADATA +1 -1
- {hyperquant-0.3.dist-info → hyperquant-0.4.dist-info}/RECORD +8 -8
- {hyperquant-0.3.dist-info → hyperquant-0.4.dist-info}/WHEEL +0 -0
hyperquant/broker/ourbit.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
import asyncio
|
1
2
|
from typing import Literal, Optional
|
2
3
|
import pybotters
|
3
|
-
from .models.ourbit import OurbitSwapDataStore
|
4
|
+
from .models.ourbit import OurbitSwapDataStore, OurbitSpotDataStore
|
5
|
+
from decimal import Decimal, ROUND_HALF_UP
|
4
6
|
|
5
7
|
|
6
8
|
class OurbitSwap:
|
@@ -32,6 +34,7 @@ class OurbitSwap:
|
|
32
34
|
f"{self.api_url}/api/v1/private/order/list/open_orders?page_size=200",
|
33
35
|
f"{self.api_url}/api/v1/private/account/assets",
|
34
36
|
f"{self.api_url}/api/v1/contract/ticker",
|
37
|
+
f"{self.api_url}/api/platform/spot/market/v2/symbols"
|
35
38
|
]
|
36
39
|
|
37
40
|
url_map = {
|
@@ -62,7 +65,7 @@ class OurbitSwap:
|
|
62
65
|
hdlr_json=self.store.onmessage
|
63
66
|
)
|
64
67
|
|
65
|
-
async def
|
68
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
66
69
|
if isinstance(symbols, str):
|
67
70
|
symbols = [symbols]
|
68
71
|
|
@@ -86,18 +89,31 @@ class OurbitSwap:
|
|
86
89
|
hdlr_json=self.store.onmessage
|
87
90
|
)
|
88
91
|
|
92
|
+
async def sub_personal(self):
|
93
|
+
self.client.ws_connect(
|
94
|
+
self.ws_url,
|
95
|
+
send_json={ "method": "sub.personal.user.preference"},
|
96
|
+
hdlr_json=self.store.onmessage
|
97
|
+
)
|
98
|
+
|
89
99
|
def ret_content(self, res: pybotters.FetchResult):
|
90
100
|
match res.data:
|
91
101
|
case {"success": True}:
|
92
102
|
return res.data["data"]
|
93
103
|
case _:
|
94
104
|
raise Exception(f"Failed api {res.response.url}: {res.data}")
|
95
|
-
|
105
|
+
|
106
|
+
|
107
|
+
def fmt_price(self, symbol, price: float) -> float:
|
108
|
+
tick = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
109
|
+
tick_dec = Decimal(str(tick))
|
110
|
+
price_dec = Decimal(str(price))
|
111
|
+
return float((price_dec / tick_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * tick_dec)
|
96
112
|
|
97
113
|
async def place_order(
|
98
114
|
self,
|
99
115
|
symbol: str,
|
100
|
-
side: Literal["buy", "sell", "
|
116
|
+
side: Literal["buy", "sell", "close_buy", "close_sell"],
|
101
117
|
size: float = None,
|
102
118
|
price: float = None,
|
103
119
|
order_type: Literal["market", "limit_GTC", "limit_IOC"] = "market",
|
@@ -105,7 +121,16 @@ class OurbitSwap:
|
|
105
121
|
leverage: Optional[int] = 20,
|
106
122
|
position_id: Optional[int] = None,
|
107
123
|
):
|
108
|
-
"""
|
124
|
+
"""
|
125
|
+
size为合约张数, openType 1 为逐仓, 2为全仓
|
126
|
+
|
127
|
+
.. code ::
|
128
|
+
{
|
129
|
+
"orderId": "219602019841167810",
|
130
|
+
"ts": 1756395601543
|
131
|
+
}
|
132
|
+
|
133
|
+
"""
|
109
134
|
if (size is None) == (usdt_amount is None):
|
110
135
|
raise ValueError("params err")
|
111
136
|
|
@@ -114,14 +139,17 @@ class OurbitSwap:
|
|
114
139
|
if usdt_amount is not None:
|
115
140
|
cs = self.store.detail.find({"symbol": symbol})[0].get("contract_sz")
|
116
141
|
size = max(int(usdt_amount / cs / price), 1)
|
142
|
+
|
143
|
+
if price is not None:
|
144
|
+
price = self.fmt_price(symbol, price)
|
117
145
|
|
118
146
|
|
119
|
-
leverage =
|
147
|
+
leverage = min(max_lev, leverage)
|
120
148
|
|
121
149
|
data = {
|
122
150
|
"symbol": symbol,
|
123
151
|
"side": 1 if side == "buy" else 3,
|
124
|
-
"openType":
|
152
|
+
"openType": 2,
|
125
153
|
"type": "5",
|
126
154
|
"vol": size,
|
127
155
|
"leverage": leverage,
|
@@ -136,16 +164,65 @@ class OurbitSwap:
|
|
136
164
|
data["type"] = "1"
|
137
165
|
data["price"] = str(price)
|
138
166
|
|
139
|
-
if
|
140
|
-
|
167
|
+
if "close" in side:
|
168
|
+
if side == 'close_buy':
|
169
|
+
data["side"] = 2
|
170
|
+
elif side == 'close_sell':
|
171
|
+
data["side"] = 4
|
172
|
+
|
141
173
|
if position_id is None:
|
142
174
|
raise ValueError("position_id is required for closing position")
|
143
175
|
data["positionId"] = position_id
|
144
|
-
|
176
|
+
# import time
|
177
|
+
# print(time.time(), '下单')
|
145
178
|
res = await self.client.fetch(
|
146
179
|
"POST", f"{self.api_url}/api/v1/private/order/create", data=data
|
147
180
|
)
|
148
181
|
return self.ret_content(res)
|
182
|
+
|
183
|
+
async def place_tpsl(self,
|
184
|
+
position_id: int,
|
185
|
+
take_profit: Optional[float] = None,
|
186
|
+
stop_loss: Optional[float] = None,
|
187
|
+
):
|
188
|
+
"""
|
189
|
+
position_id 持仓ID
|
190
|
+
|
191
|
+
.. code:: json
|
192
|
+
|
193
|
+
{
|
194
|
+
"success": true,
|
195
|
+
"code": 0,
|
196
|
+
"data": 2280508
|
197
|
+
}
|
198
|
+
"""
|
199
|
+
if (take_profit is None) and (stop_loss is None):
|
200
|
+
raise ValueError("params err")
|
201
|
+
|
202
|
+
data = {
|
203
|
+
"positionId": position_id,
|
204
|
+
"profitTrend": "1",
|
205
|
+
"lossTrend": "1",
|
206
|
+
"profitLossVolType": "SAME",
|
207
|
+
"volType": 2,
|
208
|
+
"takeProfitReverse": 2,
|
209
|
+
"stopLossReverse": 2,
|
210
|
+
"priceProtect": "0",
|
211
|
+
}
|
212
|
+
|
213
|
+
if take_profit is not None:
|
214
|
+
data["takeProfitPrice"] = take_profit
|
215
|
+
if stop_loss is not None:
|
216
|
+
data["stopLossPrice"] = stop_loss
|
217
|
+
|
218
|
+
|
219
|
+
res = await self.client.fetch(
|
220
|
+
"POST",
|
221
|
+
f"{self.api_url}/api/v1/private/stoporder/place",
|
222
|
+
data=data
|
223
|
+
)
|
224
|
+
|
225
|
+
return self.ret_content(res)
|
149
226
|
|
150
227
|
async def cancel_orders(self, order_ids: list[str]):
|
151
228
|
res = await self.client.fetch(
|
@@ -230,5 +307,194 @@ class OurbitSwap:
|
|
230
307
|
"GET",
|
231
308
|
f"{self.api_url}/api/v1/private/order/deal_details/{order_id}",
|
232
309
|
)
|
233
|
-
|
234
310
|
return self.ret_content(res)
|
311
|
+
|
312
|
+
|
313
|
+
class OurbitSpot:
|
314
|
+
|
315
|
+
def __init__(self, client: pybotters.Client):
|
316
|
+
"""
|
317
|
+
✅ 完成:
|
318
|
+
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
319
|
+
|
320
|
+
"""
|
321
|
+
self.client = client
|
322
|
+
self.store = OurbitSpotDataStore()
|
323
|
+
self.api_url = "https://www.ourbit.com"
|
324
|
+
self.ws_url = "wss://www.ourbit.com/ws"
|
325
|
+
|
326
|
+
async def __aenter__(self) -> "OurbitSpot":
|
327
|
+
client = self.client
|
328
|
+
await self.store.initialize(
|
329
|
+
client.get(f"{self.api_url}/api/platform/spot/market/v2/symbols")
|
330
|
+
)
|
331
|
+
return self
|
332
|
+
|
333
|
+
async def update(self, update_type: Literal["orders", "balance", "ticker", "book", "all"] = "all"):
|
334
|
+
|
335
|
+
all_urls = [
|
336
|
+
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",
|
337
|
+
f"{self.api_url}/api/assetbussiness/asset/spot/statistic",
|
338
|
+
f"{self.api_url}/api/platform/spot/market/v2/tickers"
|
339
|
+
]
|
340
|
+
|
341
|
+
# orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3
|
342
|
+
|
343
|
+
url_map = {
|
344
|
+
"orders": [all_urls[0]],
|
345
|
+
"balance": [all_urls[1]],
|
346
|
+
"ticker": [all_urls[2]],
|
347
|
+
"all": all_urls
|
348
|
+
}
|
349
|
+
|
350
|
+
try:
|
351
|
+
urls = url_map[update_type]
|
352
|
+
except KeyError:
|
353
|
+
raise ValueError(f"Unknown update type: {update_type}")
|
354
|
+
|
355
|
+
# 直接传协程进去,initialize 会自己 await
|
356
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
357
|
+
|
358
|
+
|
359
|
+
async def sub_personal(self):
|
360
|
+
"""订阅个人频道"""
|
361
|
+
# https://www.ourbit.com/ucenter/api/ws_token
|
362
|
+
res = await self.client.fetch(
|
363
|
+
'GET', f"{self.api_url}/ucenter/api/ws_token"
|
364
|
+
)
|
365
|
+
|
366
|
+
token = res.data['data'].get("wsToken")
|
367
|
+
|
368
|
+
|
369
|
+
self.client.ws_connect(
|
370
|
+
f"{self.ws_url}?wsToken={token}&platform=web",
|
371
|
+
send_json={
|
372
|
+
"method": "SUBSCRIPTION",
|
373
|
+
"params": [
|
374
|
+
"spot@private.orders",
|
375
|
+
"spot@private.trigger.orders",
|
376
|
+
"spot@private.balances"
|
377
|
+
],
|
378
|
+
"id": 1
|
379
|
+
},
|
380
|
+
hdlr_json=self.store.onmessage
|
381
|
+
)
|
382
|
+
|
383
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
384
|
+
"""订阅订单簿深度数据
|
385
|
+
|
386
|
+
Args:
|
387
|
+
symbols: 交易对符号,可以是单个字符串或字符串列表
|
388
|
+
"""
|
389
|
+
if isinstance(symbols, str):
|
390
|
+
symbols = [symbols]
|
391
|
+
|
392
|
+
# 并发获取每个交易对的初始深度数据
|
393
|
+
tasks = [
|
394
|
+
self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
|
395
|
+
for symbol in symbols
|
396
|
+
]
|
397
|
+
|
398
|
+
# 等待所有请求完成
|
399
|
+
responses = await asyncio.gather(*tasks)
|
400
|
+
|
401
|
+
# 处理响应数据
|
402
|
+
for response in responses:
|
403
|
+
self.store.book._onresponse(response.data)
|
404
|
+
|
405
|
+
# 构建订阅参数
|
406
|
+
subscription_params = []
|
407
|
+
for symbol in symbols:
|
408
|
+
subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
|
409
|
+
|
410
|
+
|
411
|
+
# 一次sub20个,超过需要分开订阅
|
412
|
+
for i in range(0, len(subscription_params), 20):
|
413
|
+
self.client.ws_connect(
|
414
|
+
'wss://www.ourbit.com/ws?platform=web',
|
415
|
+
send_json={
|
416
|
+
"method": "SUBSCRIPTION",
|
417
|
+
"params": subscription_params[i:i + 20],
|
418
|
+
"id": 2
|
419
|
+
},
|
420
|
+
hdlr_json=self.store.onmessage
|
421
|
+
)
|
422
|
+
|
423
|
+
async def place_order(
|
424
|
+
self,
|
425
|
+
symbol: str,
|
426
|
+
side: Literal["buy", "sell"],
|
427
|
+
price: float,
|
428
|
+
quantity: float = None,
|
429
|
+
order_type: Literal["market", "limit"] = "limit",
|
430
|
+
usdt_amount: float = None
|
431
|
+
):
|
432
|
+
"""现货下单
|
433
|
+
|
434
|
+
Args:
|
435
|
+
symbol: 交易对,如 "SOL_USDT"
|
436
|
+
side: 买卖方向 "buy" 或 "sell"
|
437
|
+
price: 价格
|
438
|
+
quantity: 数量
|
439
|
+
order_type: 订单类型 "market" 或 "limit"
|
440
|
+
usdt_amount: USDT金额,如果指定则根据价格计算数量
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
订单响应数据
|
444
|
+
"""
|
445
|
+
# 解析交易对
|
446
|
+
currency, market = symbol.split("_")
|
447
|
+
|
448
|
+
detail = self.store.detail.get({
|
449
|
+
'name': currency
|
450
|
+
})
|
451
|
+
|
452
|
+
if not detail:
|
453
|
+
raise ValueError(f"Unknown currency: {currency}")
|
454
|
+
|
455
|
+
price_scale = detail.get('price_scale')
|
456
|
+
quantity_scale = detail.get('quantity_scale')
|
457
|
+
|
458
|
+
|
459
|
+
# 如果指定了USDT金额,重新计算数量
|
460
|
+
if usdt_amount is not None:
|
461
|
+
if side == "buy":
|
462
|
+
quantity = usdt_amount / price
|
463
|
+
else:
|
464
|
+
# 卖出时usdt_amount表示要卖出的币种价值
|
465
|
+
quantity = usdt_amount / price
|
466
|
+
|
467
|
+
# 格式化价格和数量
|
468
|
+
if price_scale is not None:
|
469
|
+
price = round(price, price_scale)
|
470
|
+
|
471
|
+
if quantity_scale is not None:
|
472
|
+
quantity = round(quantity, quantity_scale)
|
473
|
+
|
474
|
+
# 构建请求数据
|
475
|
+
data = {
|
476
|
+
"currency": currency,
|
477
|
+
"market": market,
|
478
|
+
"tradeType": side.upper(),
|
479
|
+
"quantity": str(quantity),
|
480
|
+
}
|
481
|
+
|
482
|
+
if order_type == "limit":
|
483
|
+
data["orderType"] = "LIMIT_ORDER"
|
484
|
+
data["price"] = str(price)
|
485
|
+
elif order_type == "market":
|
486
|
+
data["orderType"] = "MARKET_ORDER"
|
487
|
+
# 市价单通常不需要价格参数
|
488
|
+
|
489
|
+
res = await self.client.fetch(
|
490
|
+
"POST",
|
491
|
+
f"{self.api_url}/api/platform/spot/order/place",
|
492
|
+
json=data
|
493
|
+
)
|
494
|
+
|
495
|
+
# 处理响应
|
496
|
+
match res.data:
|
497
|
+
case {"msg": 'success'}:
|
498
|
+
return res.data["data"]
|
499
|
+
case _:
|
500
|
+
raise Exception(f"Failed to place order: {res.data}")
|
hyperquant/broker/ws.py
CHANGED
@@ -1,5 +1,8 @@
|
|
1
1
|
import asyncio
|
2
2
|
import pybotters
|
3
|
+
from pybotters.ws import ClientWebSocketResponse, logger
|
4
|
+
from pybotters.auth import Hosts
|
5
|
+
import yarl
|
3
6
|
|
4
7
|
|
5
8
|
class Heartbeat:
|
@@ -8,5 +11,38 @@ class Heartbeat:
|
|
8
11
|
while not ws.closed:
|
9
12
|
await ws.send_str('{"method":"ping"}')
|
10
13
|
await asyncio.sleep(10.0)
|
14
|
+
|
15
|
+
async def ourbit_spot(ws: pybotters.ws.ClientWebSocketResponse):
|
16
|
+
while not ws.closed:
|
17
|
+
await ws.send_str('{"method":"ping"}')
|
18
|
+
await asyncio.sleep(10.0)
|
19
|
+
|
20
|
+
pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
|
21
|
+
pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
|
22
|
+
|
23
|
+
class WssAuth:
|
24
|
+
@staticmethod
|
25
|
+
async def ourbit(ws: ClientWebSocketResponse):
|
26
|
+
key: str = ws._response._session.__dict__["_apis"][
|
27
|
+
pybotters.ws.AuthHosts.items[ws._response.url.host].name
|
28
|
+
][0]
|
29
|
+
await ws.send_json(
|
30
|
+
{
|
31
|
+
"method": "login",
|
32
|
+
"param": {
|
33
|
+
"token": key
|
34
|
+
}
|
35
|
+
}
|
36
|
+
)
|
37
|
+
async for msg in ws:
|
38
|
+
# {"channel":"rs.login","data":"success","ts":1756470267848}
|
39
|
+
data = msg.json()
|
40
|
+
if data.get("channel") == "rs.login":
|
41
|
+
if data.get("data") == "success":
|
42
|
+
break
|
43
|
+
else:
|
44
|
+
logger.warning(f"WebSocket login failed: {data}")
|
45
|
+
|
46
|
+
|
11
47
|
|
12
|
-
pybotters.ws.
|
48
|
+
pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyperquant
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.4
|
4
4
|
Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
|
5
5
|
Project-URL: Homepage, https://github.com/yourusername/hyperquant
|
6
6
|
Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
|
@@ -4,18 +4,18 @@ hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
|
|
4
4
|
hyperquant/draw.py,sha256=up_lQ3pHeVLoNOyh9vPjgNwjD0M-6_IetSGviQUgjhY,54624
|
5
5
|
hyperquant/logkit.py,sha256=WALpXpIA3Ywr5DxKKK3k5EKubZ2h-ISGfc5dUReQUBQ,7795
|
6
6
|
hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
|
7
|
-
hyperquant/broker/auth.py,sha256=
|
8
|
-
hyperquant/broker/hyperliquid.py,sha256=
|
9
|
-
hyperquant/broker/ourbit.py,sha256=
|
10
|
-
hyperquant/broker/ws.py,sha256=
|
7
|
+
hyperquant/broker/auth.py,sha256=oA9Yw1I59-u0Tnoj2e4wUup5q8V5T2qpga5RKbiAiZI,2614
|
8
|
+
hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
|
9
|
+
hyperquant/broker/ourbit.py,sha256=BWXH-FQoBZoEzBKYY0mCXYW9Iy3EgHTYan4McFp4-R8,15952
|
10
|
+
hyperquant/broker/ws.py,sha256=umRzxwCaZaRIgIq4YY-AuA0wCXFT0uOBmQbIXFY8CK0,1555
|
11
11
|
hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
|
12
12
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
13
13
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
14
|
-
hyperquant/broker/models/ourbit.py,sha256
|
14
|
+
hyperquant/broker/models/ourbit.py,sha256=-XgxQ9JB-hk7r6u2CmXsNx4055kpYr0lZWh0Su6SWIA,37539
|
15
15
|
hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
|
16
16
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
17
17
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
18
18
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
19
|
-
hyperquant-0.
|
20
|
-
hyperquant-0.
|
21
|
-
hyperquant-0.
|
19
|
+
hyperquant-0.4.dist-info/METADATA,sha256=CEAtZ3dZLsujTB7cj0BGAFOd9MUKd_EDGpgru09ozww,4316
|
20
|
+
hyperquant-0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
21
|
+
hyperquant-0.4.dist-info/RECORD,,
|
File without changes
|