hyperquant 1.22__py3-none-any.whl → 1.25__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.

@@ -220,6 +220,62 @@ class Orders(DataStore):
220
220
  if items:
221
221
  self._insert(items)
222
222
 
223
+ def _on_message(self, msg: dict[str, Any]) -> None:
224
+ """Handle websocket incremental updates for orders.
225
+
226
+ For WS updates we should not clear-and-reinsert. Instead:
227
+ - For fully filled or cancelled orders => delete
228
+ - Otherwise => update/insert
229
+ """
230
+ if not isinstance(msg, dict):
231
+ return
232
+
233
+ orders_obj = msg.get("orders")
234
+ if orders_obj is None:
235
+ account = msg.get("account")
236
+ if isinstance(account, dict):
237
+ orders_obj = account.get("orders")
238
+ if not orders_obj:
239
+ return
240
+
241
+ # Normalize orders to a flat list of dicts
242
+ if isinstance(orders_obj, dict):
243
+ raw_list: list[dict[str, Any]] = []
244
+ for _, lst in orders_obj.items():
245
+ if isinstance(lst, list):
246
+ raw_list.extend([o for o in lst if isinstance(o, dict)])
247
+ elif isinstance(orders_obj, list):
248
+ raw_list = [o for o in orders_obj if isinstance(o, dict)]
249
+ else:
250
+ return
251
+
252
+ def _is_terminal(order: dict[str, Any]) -> bool:
253
+ status = str(order.get("status", "")).lower()
254
+ if status in {"cancelled", "canceled", "executed", "filled", "closed", "done"}:
255
+ return True
256
+ rem = order.get("remaining_base_amount")
257
+ try:
258
+ return float(rem) <= 0 if rem is not None else False
259
+ except Exception:
260
+ return False
261
+
262
+
263
+ for entry in raw_list:
264
+ normalized = self._normalize(entry)
265
+ if normalized is None:
266
+ continue
267
+ # enrich with symbol if mapping is available
268
+ if self.id_to_symbol:
269
+ market_id = entry.get("market_index")
270
+ if market_id is not None:
271
+ symbol = self.id_to_symbol.get(str(market_id))
272
+ if symbol is not None:
273
+ normalized["symbol"] = symbol
274
+
275
+ self._update([normalized])
276
+ if _is_terminal(entry):
277
+ self._delete([normalized])
278
+
223
279
 
224
280
 
225
281
 
@@ -324,6 +380,282 @@ class Positions(DataStore):
324
380
  self._update_positions(account_index, positions)
325
381
 
326
382
 
383
+ class Klines(DataStore):
384
+ """Candlestick/Kline store keyed by (symbol, resolution, timestamp).
385
+
386
+ - Maintains a list of active resolutions in ``_res_list`` (populated by REST updates).
387
+ - Updates candles in real-time by aggregating trade websocket messages.
388
+ """
389
+
390
+ _KEYS = ["symbol", "resolution", "timestamp"]
391
+
392
+ def _init(self) -> None:
393
+ self.id_to_symbol: dict[str, str] = {}
394
+ self._current_symbol: str | None = None
395
+ self._res_list: list[str] = []
396
+ # Track last processed trade_id to deduplicate snapshot trades after reconnect
397
+ self._last_trade_id_by_market: dict[str, int] = {}
398
+ self._last_trade_id_by_symbol: dict[str, int] = {}
399
+
400
+ @staticmethod
401
+ def _resolution_to_ms(resolution: str) -> int | None:
402
+ try:
403
+ res = resolution.strip().lower()
404
+ except Exception:
405
+ return None
406
+ # Common forms: 1m, 5m, 1h, 1d; also allow pure digits => seconds
407
+ unit = res[-1]
408
+ num_part = res[:-1] if unit in {"s", "m", "h", "d", "w"} else res
409
+ try:
410
+ n = int(num_part)
411
+ except Exception:
412
+ return None
413
+ if unit == "s":
414
+ return n * 1000
415
+ if unit == "m" or unit not in {"s", "h", "d", "w"}: # default minutes if no unit
416
+ return n * 60 * 1000
417
+ if unit == "h":
418
+ return n * 60 * 60 * 1000
419
+ if unit == "d":
420
+ return n * 24 * 60 * 60 * 1000
421
+ if unit == "w":
422
+ return n * 7 * 24 * 60 * 60 * 1000
423
+ return None
424
+
425
+ @staticmethod
426
+ def _market_id_from_channel(channel: str | None) -> str | None:
427
+ if not channel:
428
+ return None
429
+ if ":" in channel:
430
+ return channel.split(":", 1)[1]
431
+ if "/" in channel:
432
+ return channel.split("/", 1)[1]
433
+ return channel
434
+
435
+ def _compose_item(
436
+ self,
437
+ *,
438
+ symbol: str,
439
+ resolution: str,
440
+ ts: int,
441
+ price: float,
442
+ size: float,
443
+ last_trade_id: int | None,
444
+ open_price: float | None = None,
445
+ ) -> dict[str, Any]:
446
+ return {
447
+ "symbol": symbol,
448
+ "resolution": resolution,
449
+ "timestamp": ts,
450
+ "open": price if open_price is None else float(open_price),
451
+ "high": price,
452
+ "low": price,
453
+ "close": price,
454
+ "volume0": abs(size),
455
+ "volume1": abs(size) * price,
456
+ "last_trade_id": last_trade_id or 0,
457
+ }
458
+
459
+ def _ensure_backfill(self, *, symbol: str, resolution: str, new_bucket_ts: int) -> None:
460
+ """Backfill missing empty bars up to (but not including) new_bucket_ts.
461
+
462
+ Uses the last known close as O/H/L/C for synthetic bars and zero volume.
463
+ """
464
+ step = self._resolution_to_ms(resolution)
465
+ if not step:
466
+ return
467
+ # find the last existing bar before new_bucket_ts
468
+ rows = self.find({"symbol": symbol, "resolution": resolution})
469
+ prev = None
470
+ prev_ts = None
471
+ for r in rows:
472
+ try:
473
+ ts = int(r.get("timestamp"))
474
+ except Exception:
475
+ continue
476
+ if ts < new_bucket_ts and (prev_ts is None or ts > prev_ts):
477
+ prev = r
478
+ prev_ts = ts
479
+ if prev is None or prev_ts is None:
480
+ return
481
+ expected = prev_ts + step
482
+ while expected < new_bucket_ts:
483
+ prev_close = float(prev.get("close"))
484
+ fill_item = {
485
+ "symbol": symbol,
486
+ "resolution": resolution,
487
+ "timestamp": expected,
488
+ "open": prev_close,
489
+ "high": prev_close,
490
+ "low": prev_close,
491
+ "close": prev_close,
492
+ "volume0": 0.0,
493
+ "volume1": 0.0,
494
+ "last_trade_id": int(prev.get("last_trade_id", 0)) if prev.get("last_trade_id") is not None else 0,
495
+ }
496
+ self._insert([fill_item])
497
+ prev = fill_item
498
+ expected += step
499
+
500
+ def _merge_trade(self, *, symbol: str, trade_ts_ms: int, price: float, size: float, last_trade_id: int | None) -> None:
501
+ # Iterate active resolutions
502
+ for res in list(self._res_list):
503
+ interval_ms = self._resolution_to_ms(res)
504
+ if not interval_ms:
505
+ continue
506
+ bucket_ts = (trade_ts_ms // interval_ms) * interval_ms
507
+ # Upsert logic
508
+ existing = self.get({"symbol": symbol, "resolution": res, "timestamp": bucket_ts})
509
+ if existing is None:
510
+ # backfill any missing empty bars before creating a new bucket
511
+ self._ensure_backfill(symbol=symbol, resolution=res, new_bucket_ts=bucket_ts)
512
+ # open should be previous bar's close if exists; if none, fall back to current price
513
+ prev = None
514
+ rows = self.find({"symbol": symbol, "resolution": res})
515
+ prev_ts = None
516
+ for r in rows:
517
+ try:
518
+ ts = int(r.get("timestamp"))
519
+ except Exception:
520
+ continue
521
+ if ts < bucket_ts and (prev_ts is None or ts > prev_ts):
522
+ prev = r
523
+ prev_ts = ts
524
+ open_px = float(prev.get("close")) if prev is not None else price
525
+ self._insert([
526
+ self._compose_item(
527
+ symbol=symbol,
528
+ resolution=res,
529
+ ts=bucket_ts,
530
+ price=price,
531
+ size=size,
532
+ last_trade_id=last_trade_id,
533
+ open_price=open_px,
534
+ )
535
+ ])
536
+ continue
537
+ # merge into existing
538
+ updated = dict(existing)
539
+ o = float(updated.get("open", price))
540
+ h = float(updated.get("high", price))
541
+ l = float(updated.get("low", price))
542
+ c = float(updated.get("close", price))
543
+ v0 = float(updated.get("volume0", 0.0))
544
+ v1 = float(updated.get("volume1", 0.0))
545
+ p = float(price)
546
+ s = abs(float(size))
547
+ updated["open"] = o
548
+ updated["high"] = max(h, p)
549
+ updated["low"] = min(l, p)
550
+ updated["close"] = p
551
+ updated["volume0"] = v0 + s
552
+ updated["volume1"] = v1 + s * p
553
+ if last_trade_id is not None:
554
+ try:
555
+ updated["last_trade_id"] = max(int(last_trade_id), int(updated.get("last_trade_id", 0)))
556
+ except Exception:
557
+ updated["last_trade_id"] = int(last_trade_id)
558
+ self._update([updated])
559
+
560
+ def _onresponse(self, data: Any, *, symbol: str | None = None, resolution: str | None = None) -> None:
561
+ payload = _maybe_to_dict(data) or {}
562
+ candlesticks = payload.get("candlesticks") or []
563
+ res = payload.get("resolution") or resolution
564
+ if res not in self._res_list and res is not None:
565
+ self._res_list.append(res)
566
+
567
+ sym = symbol or self._current_symbol
568
+
569
+ # Sort incoming bars by timestamp to backfill in order
570
+ items: list[dict[str, Any]] = []
571
+ for c in sorted((candlesticks or []), key=lambda x: x.get("timestamp", 0)):
572
+ if not isinstance(c, dict):
573
+ continue
574
+ entry = dict(c)
575
+ if sym is not None:
576
+ entry["symbol"] = sym
577
+ if res is not None:
578
+ entry["resolution"] = res
579
+ items.append(entry)
580
+
581
+ # Insert or update per bar; backfill gaps before inserting new bars
582
+ for entry in items:
583
+ sym_i = entry.get("symbol")
584
+ res_i = entry.get("resolution")
585
+ ts_i = entry.get("timestamp")
586
+ if sym_i is None or res_i is None or ts_i is None:
587
+ continue
588
+ if self.get({"symbol": sym_i, "resolution": res_i, "timestamp": ts_i}) is None:
589
+ self._ensure_backfill(symbol=sym_i, resolution=res_i, new_bucket_ts=int(ts_i))
590
+ self._insert([entry])
591
+ else:
592
+ self._update([entry])
593
+
594
+ # Update last_trade_id baseline (by symbol) from REST bars if available
595
+ if sym is not None:
596
+ max_tid = 0
597
+ for e in items:
598
+ try:
599
+ tid = int(e.get("last_trade_id", 0))
600
+ except Exception:
601
+ tid = 0
602
+ if tid > max_tid:
603
+ max_tid = tid
604
+ if max_tid:
605
+ prev = self._last_trade_id_by_symbol.get(sym, 0)
606
+ if max_tid > prev:
607
+ self._last_trade_id_by_symbol[sym] = max_tid
608
+
609
+ def _on_message(self, msg: dict[str, Any]) -> None:
610
+ msg_type = msg.get("type")
611
+ if msg_type not in {"subscribed/trade", "update/trade"}:
612
+ return
613
+ market_id = self._market_id_from_channel(msg.get("channel"))
614
+ if market_id is None:
615
+ return
616
+ market_id_str = str(market_id)
617
+ symbol = self.id_to_symbol.get(market_id_str) or market_id_str
618
+ trades = msg.get("trades") or []
619
+ # Baseline last trade_id from market and symbol
620
+ base_last_tid = max(
621
+ self._last_trade_id_by_market.get(market_id_str, 0),
622
+ self._last_trade_id_by_symbol.get(symbol, 0),
623
+ )
624
+ # Process in ascending trade_id order for stability
625
+ try:
626
+ trades_sorted = sorted(trades, key=lambda x: int(x.get("trade_id", 0)))
627
+ except Exception:
628
+ trades_sorted = trades
629
+
630
+ last_tid = base_last_tid
631
+ for t in trades_sorted:
632
+ if not isinstance(t, dict):
633
+ continue
634
+ ts = t.get("timestamp")
635
+ price = t.get("price")
636
+ size = t.get("size")
637
+ trade_id = t.get("trade_id")
638
+ try:
639
+ ts = int(ts)
640
+ p = float(price)
641
+ s = float(size)
642
+ tid = int(trade_id) if trade_id is not None else 0
643
+ except Exception:
644
+ continue
645
+ # Skip stale or duplicate snapshot trades
646
+ if tid and last_tid and tid <= last_tid:
647
+ continue
648
+ self._merge_trade(symbol=symbol, trade_ts_ms=ts, price=p, size=s, last_trade_id=tid)
649
+ if tid > last_tid:
650
+ last_tid = tid
651
+
652
+ # Persist last processed trade_id for this market
653
+ if last_tid and last_tid > base_last_tid:
654
+ self._last_trade_id_by_market[market_id_str] = last_tid
655
+
656
+
657
+
658
+
327
659
  class LighterDataStore(DataStoreCollection):
328
660
  """Data store collection for the Lighter exchange."""
329
661
 
@@ -333,12 +665,14 @@ class LighterDataStore(DataStoreCollection):
333
665
  self._create("orders", datastore_class=Orders)
334
666
  self._create("accounts", datastore_class=Accounts)
335
667
  self._create("positions", datastore_class=Positions)
668
+ self._create("klines", datastore_class=Klines)
336
669
 
337
670
  def set_id_to_symbol(self, id_to_symbol: dict[str, str]) -> None:
338
671
  self.id_to_symbol = id_to_symbol
339
672
  self.book.id_to_symbol = self.id_to_symbol
340
673
  self.orders.id_to_symbol = self.id_to_symbol
341
674
  self.positions.id_to_symbol = self.id_to_symbol
675
+ self.klines.id_to_symbol = self.id_to_symbol
342
676
 
343
677
  def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
344
678
 
@@ -354,6 +688,11 @@ class LighterDataStore(DataStoreCollection):
354
688
  elif msg_type in {"subscribed/account_all", "update/account_all"}:
355
689
  self.accounts._on_message(msg)
356
690
  self.positions._on_message(msg)
691
+ self.orders._on_message(msg)
692
+ elif msg_type in {"subscribed/account_all_orders", "update/account_all_orders"}:
693
+ self.orders._on_message(msg)
694
+ elif msg_type in {"subscribed/trade", "update/trade"}:
695
+ self.klines._on_message(msg)
357
696
 
358
697
 
359
698
  @property
@@ -506,3 +845,24 @@ class LighterDataStore(DataStoreCollection):
506
845
  ]
507
846
  """
508
847
  return self._get("positions")
848
+
849
+ @property
850
+ def klines(self) -> "Klines":
851
+ """
852
+ K线/蜡烛图数据(`lighter.models.Candlesticks` -> `lighter.models.Candlestick`)。
853
+
854
+ .. code:: json
855
+
856
+ {
857
+ "symbol": "BTC",
858
+ "timestamp": 1730612700000,
859
+ "open": 68970.5,
860
+ "high": 69012.3,
861
+ "low": 68890.0,
862
+ "close": 68995.1,
863
+ "volume0": 12.34,
864
+ "volume1": 850000.0,
865
+ "resolution": "1m"
866
+ }
867
+ """
868
+ return self._get("klines")
hyperquant/broker/ws.py CHANGED
@@ -37,12 +37,19 @@ class Heartbeat:
37
37
  while not ws.closed:
38
38
  await ws.send_json({"event": "ping"})
39
39
  await asyncio.sleep(3.0)
40
+
41
+ @staticmethod
42
+ async def lighter(ws: ClientWebSocketResponse):
43
+ while not ws.closed:
44
+ await ws.send_json({"type":"ping"})
45
+ await asyncio.sleep(3)
40
46
 
41
47
  pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
42
48
  pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
43
49
  pybotters.ws.HeartbeatHosts.items['quote.edgex.exchange'] = Heartbeat.edgex
44
50
  pybotters.ws.HeartbeatHosts.items['uuws.rerrkvifj.com'] = Heartbeat.lbank
45
51
  pybotters.ws.HeartbeatHosts.items['ws.futurescw.com'] = Heartbeat.coinw
52
+ pybotters.ws.HeartbeatHosts.items['mainnet.zklighter.elliot.ai'] = Heartbeat.lighter
46
53
 
47
54
  class WssAuth:
48
55
  @staticmethod
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.22
3
+ Version: 1.25
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
@@ -16,15 +16,12 @@ Requires-Python: >=3.11
16
16
  Requires-Dist: aiohttp>=3.10.4
17
17
  Requires-Dist: colorama>=0.4.6
18
18
  Requires-Dist: cryptography>=44.0.2
19
- Requires-Dist: curl-cffi>=0.13.0
20
19
  Requires-Dist: duckdb>=1.2.2
21
20
  Requires-Dist: lighter-sdk
22
- Requires-Dist: numba>=0.62.1
23
21
  Requires-Dist: numpy>=1.21.0
24
22
  Requires-Dist: pandas>=2.2.3
25
23
  Requires-Dist: pybotters>=1.9.1
26
24
  Requires-Dist: pyecharts>=2.0.8
27
- Requires-Dist: rnet==3.0.0rc10
28
25
  Description-Content-Type: text/markdown
29
26
 
30
27
  # minquant
@@ -7,31 +7,29 @@ hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
7
7
  hyperquant/broker/auth.py,sha256=3Ws_2iaF2NTCVIbkLzL_0ae3y7f4vgj9zmozvGaUXnk,16029
8
8
  hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
9
9
  hyperquant/broker/bitmart.py,sha256=7j_8TU3Dxjj5HCNX7CbSO3nPZcQH1t31A9UOv5tTbg0,25974
10
- hyperquant/broker/coinup.py,sha256=eOr8BTRXiTb5tCU2FDmvBdXXgqiwVmCbP5pdeA1ORJ8,20390
11
10
  hyperquant/broker/coinw.py,sha256=SnJU0vASh77rfcpMGWaIfTblQSjQk3vjlW_4juYdbcs,17214
12
11
  hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
13
12
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
14
13
  hyperquant/broker/lbank.py,sha256=98M5wmSoeHwbBYMA3rh25zqLb6fQKVaEmwqALF5nOvY,22181
15
- hyperquant/broker/lighter.py,sha256=lW4LgEJHqbiUZPu2BE1aKz_JBIW94KTKnCkD9vuHt-Q,16695
14
+ hyperquant/broker/lighter.py,sha256=YuMR6pCWhJjZ2tGMbYanATvbV6t1J22-pyHqO1GNr2g,25639
16
15
  hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
17
- hyperquant/broker/ws.py,sha256=NS71Do-62mtVrGcyNE-AtHJkDecsSxdz_KU1yNBr_BQ,4079
16
+ hyperquant/broker/ws.py,sha256=AzyFAHIDF4exxwm_IAEV6ihftwAlu19al8Vla4ygk-A,4354
18
17
  hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
19
18
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
20
19
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
21
20
  hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
22
21
  hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
23
22
  hyperquant/broker/models/bitmart.py,sha256=O9RnU-XBeR9SzicG15jzuzK5oy2kMrRJAyZSqC8DXUw,21938
24
- hyperquant/broker/models/coinup.py,sha256=X_ngB2_sgTOdfAZqTyeWvCN03j-0_inZ6ugZKW6hR7k,11173
25
23
  hyperquant/broker/models/coinw.py,sha256=LvLMVP7i-qkkTK1ubw8eBkMK2RQmFoKPxdKqmC4IToY,22157
26
24
  hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
27
25
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
28
26
  hyperquant/broker/models/lbank.py,sha256=vHkNKxIMzpoC_EwcZnEOPOupizF92yGWi9GKxvYYFUQ,19181
29
- hyperquant/broker/models/lighter.py,sha256=RpqyMPrXbs4_OY9WSDep4T8pDhxDGaFQ8vdVmLZnfBg,16732
27
+ hyperquant/broker/models/lighter.py,sha256=I6hjM0of8NLtjSmI6OlbJlvpYDDswLn9yyQLz0I1Mes,30495
30
28
  hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
31
29
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
32
30
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
33
31
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
34
32
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
35
- hyperquant-1.22.dist-info/METADATA,sha256=dH0APcdAjkH7pyeQiyPyTd3tl1E2tQVYxMd5YlbzjrE,4438
36
- hyperquant-1.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- hyperquant-1.22.dist-info/RECORD,,
33
+ hyperquant-1.25.dist-info/METADATA,sha256=oR0A52JvaP4b2W20fQgZpZaoVGaTjtACFMgyQhNB0W0,4345
34
+ hyperquant-1.25.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ hyperquant-1.25.dist-info/RECORD,,