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.
@@ -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 sub_order_book(self, symbols: str | list[str]):
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", "close"],
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
- """size为合约张数"""
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 = max(max_lev, 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": 1,
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 side == "close":
140
- data["side"] = 4
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.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
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
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=hrdVuBTZwD3xSy5GI5JTFJhKiHhvIjZHA_7G5IUFznQ,1580
8
- hyperquant/broker/hyperliquid.py,sha256=R_PZn5D-OFAvqsFnom4kUWHqbKPltdjura0VVvQvc2A,23265
9
- hyperquant/broker/ourbit.py,sha256=2_Dbs-E_c-IZKMfFTy8yhkOUnoXq8G5XMkpPm1ZYQUI,7679
10
- hyperquant/broker/ws.py,sha256=98Djt5n5sHUJKVbQ8Ql1t-G-Wiwu__4MYcUr5P6SDL0,326
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=zvjtx6fmOuOPAJ8jxD2RK9_mao2XuE2EXkxR74nmXKM,18525
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.3.dist-info/METADATA,sha256=Fn_RGo3ffo7nq4-GxrvQjgJWpNmKjmWlK6SNhgY7Yaw,4316
20
- hyperquant-0.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- hyperquant-0.3.dist-info/RECORD,,
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,,