hyperquant 0.43__py3-none-any.whl → 0.45__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.
@@ -697,69 +697,85 @@ class SpotBook(DataStore):
697
697
  # super().__init__()
698
698
  self._time: int | None = None
699
699
  self.limit = 1
700
+ self.loss = True
701
+ self.version = None
700
702
 
701
703
  def _onresponse(self, data: dict[str, Any]):
704
+ data = data.get("data")
705
+ symbol = data.get("symbol")
706
+ book_data = data.get("data")
707
+ asks = book_data.get("asks", [])
708
+ bids = book_data.get("bids", [])
709
+ version = int(data.get("version", 0))
710
+
711
+ # 保存当前快照版本
712
+ self.version = version
713
+
714
+ # 应用缓存的增量(只保留连续的部分)
715
+ items: list = self.find({"s": symbol})
716
+ items.sort(key=lambda x: x.get("fv", 0)) # 按 fromVersion 排序
702
717
 
703
- top = data.get("data") or data.get("d") or data
704
- symbol = (
705
- top.get("s")
706
- or top.get("symbol")
707
- or (top.get("data") or {}).get("symbol")
708
- )
709
-
710
- inner = top.get("data") or top
711
- asks = inner.get("asks") or []
712
- bids = inner.get("bids") or []
713
-
714
- items: list[Item] = []
715
- if symbol:
716
- # Snapshot semantics: rebuild entries for this symbol
717
- self._find_and_delete({"s": symbol})
718
-
719
- def extract_pq(level: Any) -> tuple[Any, Any] | None:
720
- # Accept dict {"p": x, "q": y} or list/tuple [p, q, ...]
721
- if isinstance(level, dict):
722
- p = level.get("p")
723
- q = level.get("q")
724
- return (p, q)
725
- if isinstance(level, (list, tuple)) and len(level) >= 2:
726
- return (level[0], level[1])
727
- return None
728
-
718
+ # 清空旧数据,插入快照
719
+ self._find_and_delete({"s": symbol})
720
+
729
721
  for side, S in ((asks, "a"), (bids, "b")):
730
- for level in side:
731
- pq = extract_pq(level)
732
- if not pq:
733
- continue
734
- p, q = pq
735
- if p is None or q is None:
736
- continue
737
- try:
738
- if float(q) == 0.0:
739
- continue
740
- except (TypeError, ValueError):
741
- continue
742
- items.append({"s": symbol, "S": S, "p": p, "q": q})
722
+ for item in side:
723
+ self._insert([{"s": symbol, "S": S, "p": item["p"], "q": item["q"]}])
743
724
 
744
725
  if items:
745
- self._insert(items)
746
-
747
- sort_data = self.sorted({'s': symbol}, self.limit)
748
- asks = sort_data.get('a', [])
749
- bids = sort_data.get('b', [])
750
- self._find_and_delete({'s': symbol})
751
- self._update(asks + bids)
752
-
726
+ min_version = min(item.get("fv", 0) for item in items)
727
+ max_version = max(item.get("tv", 0) for item in items)
728
+ self.version = max_version
729
+
730
+ if not (min_version <= self.version <= max_version):
731
+ self.loss = True
732
+ logger.warning(f"SpotBook: Snapshot version {self.version} out of range ({min_version}, {max_version}) for symbol={symbol} (丢补丁)")
733
+ return
734
+
735
+ self.loss = False
736
+ for item in items:
737
+ fv, tv = item.get("fv", 0), item.get("tv", 0)
738
+ if self.version <= tv and self.version >= fv:
739
+ self._insert([{ "s": symbol, "S": item["S"], "p": item["p"], "q": item["q"]}])
740
+ else:
741
+ self.loss = False
753
742
 
754
743
 
755
744
  def _on_message(self, msg: dict[str, Any]) -> None:
745
+
756
746
  ts = time.time() * 1000 # 预留时间戳(如需记录可用)
757
747
  data = msg.get("d", {}) or {}
758
748
  symbol = msg.get("s")
759
-
749
+ fv = int(data.get("fromVersion"))
750
+ tv = int(data.get("toVersion"))
760
751
  asks: list = data.get("asks", []) or []
761
752
  bids: list = data.get("bids", []) or []
762
753
 
754
+ # 以下几张情况都会被认为正常
755
+ check_con = (
756
+ self.version is None or
757
+ fv <= self.version <= tv or
758
+ self.version + 1 == fv
759
+ )
760
+
761
+ if not check_con:
762
+ logger.warning(f"(丢补丁) version:{self.version} fv:{fv} tv:{tv} msg:\n {msg}")
763
+ # self.loss = True # 暂时不这样做
764
+ return
765
+
766
+ self.version = tv
767
+
768
+ if self.loss:
769
+ for item in asks:
770
+ self._insert(
771
+ [{"s": symbol, "S": "a", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv}]
772
+ )
773
+ for item in bids:
774
+ self._insert(
775
+ [{"s": symbol, "S": "b", "p": item["p"], "q": item["q"], "fv": fv, "tv": tv}]
776
+ )
777
+ return
778
+
763
779
  to_delete, to_update = [], []
764
780
  for side, S in ((asks, "a"), (bids, "b")):
765
781
  for item in side:
@@ -777,7 +793,7 @@ class SpotBook(DataStore):
777
793
  self._find_and_delete({'s': symbol})
778
794
  self._update(asks + bids)
779
795
 
780
- # print(f'处理耗时: {time.time()*1000 - ts:.2f} ms')
796
+ print(f'处理耗时: {time.time()*1000 - ts:.2f} ms')
781
797
 
782
798
 
783
799
 
@@ -389,31 +389,20 @@ class OurbitSpot:
389
389
  Args:
390
390
  symbols: 交易对符号,可以是单个字符串或字符串列表
391
391
  """
392
+ import logging
393
+ logger = logging.getLogger("OurbitSpot")
394
+
392
395
  if isinstance(symbols, str):
393
396
  symbols = [symbols]
394
397
 
395
- # 并发获取每个交易对的初始深度数据
396
- tasks = [
397
- self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
398
- for symbol in symbols
399
- ]
400
-
401
- # 等待所有请求完成
402
- responses = await asyncio.gather(*tasks)
403
-
404
- # 处理响应数据
405
- for response in responses:
406
- self.store.book._onresponse(response.data)
407
-
408
398
  # 构建订阅参数
409
399
  subscription_params = []
410
400
  for symbol in symbols:
411
401
  subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
412
402
 
413
-
414
403
  # 一次sub20个,超过需要分开订阅
415
404
  for i in range(0, len(subscription_params), 20):
416
- self.client.ws_connect(
405
+ wsapp = self.client.ws_connect(
417
406
  'wss://www.ourbit.com/ws?platform=web',
418
407
  send_json={
419
408
  "method": "SUBSCRIPTION",
@@ -422,12 +411,38 @@ class OurbitSpot:
422
411
  },
423
412
  hdlr_json=self.store.onmessage
424
413
  )
414
+ await wsapp._event.wait()
415
+
416
+ # await asyncio.sleep(2) # 等待ws连接稳定
417
+
418
+ # 并发获取每个交易对的初始深度数据
419
+ tasks = [
420
+ self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
421
+ for symbol in symbols
422
+ ]
423
+
424
+ # 等待所有请求完成
425
+ responses = await asyncio.gather(*tasks)
426
+
427
+ # 处理响应数据
428
+ for idx, response in enumerate(responses):
429
+ symbol = symbols[idx]
430
+ self.store.book._onresponse(response.data)
431
+ # Check if initialization failed (loss is False), retry if so
432
+ retry_count = 0
433
+ while self.store.book.loss is True:
434
+ logger.warning(f"Orderbook initialization failed for {symbol}, refetching depth snapshot (retry {retry_count+1})")
435
+ await asyncio.sleep(0.1)
436
+ response_retry = await self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
437
+ self.store.book._onresponse(response_retry.data)
438
+ retry_count += 1
439
+
425
440
 
426
441
  async def place_order(
427
442
  self,
428
443
  symbol: str,
429
444
  side: Literal["buy", "sell"],
430
- price: float,
445
+ price: float = None,
431
446
  quantity: float = None,
432
447
  order_type: Literal["market", "limit"] = "limit",
433
448
  usdt_amount: float = None
@@ -437,7 +452,7 @@ class OurbitSpot:
437
452
  Args:
438
453
  symbol: 交易对,如 "SOL_USDT"
439
454
  side: 买卖方向 "buy" 或 "sell"
440
- price: 价格
455
+ price: 价格,市价单可为None
441
456
  quantity: 数量
442
457
  order_type: 订单类型 "market" 或 "limit"
443
458
  usdt_amount: USDT金额,如果指定则根据价格计算数量
@@ -445,74 +460,72 @@ class OurbitSpot:
445
460
  Returns:
446
461
  订单响应数据
447
462
  """
463
+ # 参数检查
464
+ if order_type == "limit" and price is None:
465
+ raise ValueError("Limit orders require a price")
466
+ if quantity is None and usdt_amount is None:
467
+ raise ValueError("Either quantity or usdt_amount must be specified")
468
+
448
469
  # 解析交易对
449
- currency, market = symbol.split("_")
450
-
451
- detail = self.store.detail.get({
452
- 'name': currency
453
- })
454
-
470
+ parts = symbol.split("_")
471
+ if len(parts) != 2:
472
+ raise ValueError(f"Invalid symbol format: {symbol}")
473
+
474
+ currency, market = parts
475
+
476
+ # 获取交易对详情
477
+ detail = self.store.detail.get({"name": currency})
455
478
  if not detail:
456
479
  raise ValueError(f"Unknown currency: {currency}")
457
-
458
- price_scale = detail.get('price_scale')
459
- quantity_scale = detail.get('quantity_scale')
460
-
461
- use_quantity = True if quantity is not None else False
462
-
463
- # 如果指定了USDT金额,重新计算数量
464
- if usdt_amount is not None:
465
- if side == "buy":
466
- quantity = usdt_amount / price
467
- else:
468
- # 卖出时usdt_amount表示要卖出的币种价值
469
- quantity = usdt_amount / price
470
-
471
- # 格式化价格和数量
472
- if price_scale is not None:
473
- price = round(price, price_scale)
474
-
475
- if quantity_scale is not None:
476
- quantity = round(quantity, quantity_scale)
480
+
481
+ price_scale = detail.get("price_scale")
482
+ quantity_scale = detail.get("quantity_scale")
477
483
 
478
484
  # 构建请求数据
479
485
  data = {
480
486
  "currency": currency,
481
487
  "market": market,
482
- "tradeType": side.upper(),
483
- "quantity": str(quantity),
488
+ "tradeType": side.upper()
484
489
  }
485
490
 
491
+ # 处理市价单和限价单的不同参数
486
492
  if order_type == "limit":
487
493
  data["orderType"] = "LIMIT_ORDER"
488
- data["price"] = str(price)
494
+ data["price"] = str(round(price, price_scale) if price_scale is not None else price)
495
+
496
+ # 计算并设置数量
497
+ if quantity is None and usdt_amount is not None and price:
498
+ quantity = usdt_amount / price
499
+
500
+ if quantity_scale is not None:
501
+ quantity = round(quantity, quantity_scale)
502
+ data["quantity"] = str(quantity)
503
+
489
504
  elif order_type == "market":
490
505
  data["orderType"] = "MARKET_ORDER"
491
- data["price"] = price
492
- # 删除quantity, 市价只支持amount
493
- if not use_quantity:
494
- del data["quantity"]
506
+
507
+ # 市价单可以使用数量或金额,但不能同时使用
508
+ if usdt_amount is not None:
495
509
  data["amount"] = str(usdt_amount)
496
-
497
- # print(data)
498
- if order_type == 'market':
499
- url = f'{self.api_url}/api/platform/spot/v4/order/place'
500
- elif order_type == 'limit':
501
- url = f'{self.api_url}/api/platform/spot/order/place'
510
+ else:
511
+ if quantity_scale is not None:
512
+ quantity = round(quantity, quantity_scale)
513
+ data["quantity"] = str(quantity)
514
+
515
+ if price:
516
+ data["price"] = str(price)
502
517
 
503
- res = await self.client.fetch(
504
- "POST",
505
- url,
506
- json=data
507
- )
518
+ # 确定API端点
519
+ url = f'{self.api_url}/api/platform/spot/{"v4/order/place" if order_type == "market" else "order/place"}'
520
+
521
+ # 发送请求
522
+ res = await self.client.fetch("POST", url, json=data)
508
523
 
509
524
  # 处理响应
510
- match res.data:
511
- case {"msg": 'success'}:
512
- return res.data["data"]
513
- case _:
514
- raise Exception(f"Failed to place order: {res.data}")
515
-
525
+ if res.data.get("msg") == "success":
526
+ return res.data["data"]
527
+ raise Exception(f"Failed to place order: {res.data}")
528
+
516
529
  async def cancel_orders(self, order_ids: list[str]):
517
530
 
518
531
  for order_id in order_ids:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.43
3
+ Version: 0.45
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
@@ -6,16 +6,16 @@ hyperquant/logkit.py,sha256=WALpXpIA3Ywr5DxKKK3k5EKubZ2h-ISGfc5dUReQUBQ,7795
6
6
  hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
7
7
  hyperquant/broker/auth.py,sha256=oA9Yw1I59-u0Tnoj2e4wUup5q8V5T2qpga5RKbiAiZI,2614
8
8
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
9
- hyperquant/broker/ourbit.py,sha256=aBRXLmUbiiMp1__xlRufTnqMajeknj_V71iCnB87u84,16641
9
+ hyperquant/broker/ourbit.py,sha256=BJvYjl4eusmtNRbLtnOyZaSla3SNBpOhdkCvCm1KseU,17786
10
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=v675rOYlZlEYim4sYlhbmuzl9GEg4n6mleGTNMgJKes,37726
14
+ hyperquant/broker/models/ourbit.py,sha256=z3Hd6J3sFYom7DMO3VI_bf3JsU0is2ase-XM58oKNok,38544
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.43.dist-info/METADATA,sha256=dW6eimLG8hJYsHdaNu-MQzLdGjcoudusp6SyL_KLVcI,4317
20
- hyperquant-0.43.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- hyperquant-0.43.dist-info/RECORD,,
19
+ hyperquant-0.45.dist-info/METADATA,sha256=lhEnNY5bwlHfu_hyBBd9f3OX5gP4BkgiwR_TmzpLdYk,4317
20
+ hyperquant-0.45.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ hyperquant-0.45.dist-info/RECORD,,