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

@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  import json
6
+ from decimal import Decimal, ROUND_DOWN, ROUND_HALF_UP
6
7
  from typing import Any, Literal, Sequence
7
8
 
8
9
  import pybotters
@@ -296,19 +297,44 @@ class Lighter:
296
297
  self.ws_url,
297
298
  send_json=send_payload,
298
299
  hdlr_json=self.store.onmessage,
299
- autoping=False,
300
300
  )
301
301
 
302
302
  await ws_app._event.wait()
303
303
  return ws_app
304
304
 
305
- async def sub_accounts(
305
+
306
+ async def sub_orders(
306
307
  self,
307
- account_ids: Sequence[int] | int,
308
+ account_ids: Sequence[int] | int = None,
308
309
  ) -> pybotters.ws.WebSocketApp:
309
- """Subscribe to account-only websocket updates."""
310
+ """Subscribe to order updates via Account All Orders stream.
310
311
 
311
- return await self.sub_orderbook(market_ids=[], account_ids=account_ids)
312
+ Channel per docs: "account_all_orders/{ACCOUNT_ID}" (requires auth).
313
+ Response carries an "orders" mapping of market_id -> [Order].
314
+ """
315
+ if account_ids:
316
+ if isinstance(account_ids, int):
317
+ account_id_list = [str(account_ids)]
318
+ else:
319
+ account_id_list = [str(aid) for aid in account_ids]
320
+ else:
321
+ account_id_list = [self.account_index]
322
+
323
+ channels = [f"account_all_orders/{aid}" for aid in account_id_list]
324
+ send_payload = [
325
+ {"type": "subscribe", "channel": channel, "auth": self.auth}
326
+ for channel in channels
327
+ ]
328
+
329
+ ws_app = self.client.ws_connect(
330
+ self.ws_url,
331
+ send_json=send_payload,
332
+ hdlr_json=self.store.onmessage,
333
+ )
334
+ await ws_app._event.wait()
335
+ return ws_app
336
+
337
+
312
338
 
313
339
  async def sub_kline(
314
340
  self,
@@ -441,30 +467,81 @@ class Lighter:
441
467
  except KeyError as exc:
442
468
  raise ValueError(f"Unsupported time_in_force: {time_in_force}") from exc
443
469
 
444
- expiry = order_expiry if order_expiry is not None else self.signer.DEFAULT_28_DAY_ORDER_EXPIRY
470
+ # Per WS/API docs, OrderExpiry can be 0 with ExpiredAt computed by signer.
471
+ # Use caller-provided value if given; otherwise default to 0 to avoid
472
+ # "OrderExpiry is invalid" errors on some markets.
473
+ expiry = order_expiry if order_expiry is not None else 0
445
474
  nonce_value = nonce if nonce is not None else -1
446
475
  api_key_idx = api_key_index if api_key_index is not None else self.api_key_index
447
476
 
477
+ # ----- Precision and min constraints handling -----
478
+ # Prefer explicitly supported decimals. Avoid using quote decimals to infer size.
448
479
  price_decimals = (
449
480
  detail.get("supported_price_decimals")
450
481
  or detail.get("price_decimals")
451
- or detail.get("quote_decimals")
452
482
  or 0
453
483
  )
454
484
  size_decimals = (
455
485
  detail.get("supported_size_decimals")
456
486
  or detail.get("size_decimals")
457
- or detail.get("supported_quote_decimals")
458
487
  or 0
459
488
  )
460
489
 
490
+ # Optional constraints provided by the API
491
+ # Strings like "10.000000" may be returned – normalize via Decimal for accuracy
492
+ def _to_decimal(v, default: str | int = 0):
493
+ try:
494
+ if v is None or v == "":
495
+ return Decimal(str(default))
496
+ return Decimal(str(v))
497
+ except Exception:
498
+ return Decimal(str(default))
499
+
500
+ min_base_amount = _to_decimal(detail.get("min_base_amount"), 0)
501
+ min_quote_amount = _to_decimal(detail.get("min_quote_amount"), 0)
502
+ order_quote_limit = _to_decimal(detail.get("order_quote_limit"), 0)
503
+
504
+ # Use Decimal for precise arithmetic and quantization
505
+ d_price = Decimal(str(price))
506
+ d_size = Decimal(str(base_amount))
507
+ quant_price = Decimal(1) / (Decimal(10) ** int(price_decimals)) if int(price_decimals) > 0 else Decimal(1)
508
+ quant_size = Decimal(1) / (Decimal(10) ** int(size_decimals)) if int(size_decimals) > 0 else Decimal(1)
509
+
510
+ # Round price/size to allowed decimals (half up to the nearest tick)
511
+ d_price = d_price.quantize(quant_price, rounding=ROUND_HALF_UP)
512
+ d_size = d_size.quantize(quant_size, rounding=ROUND_HALF_UP)
513
+
514
+ # Ensure minimum notional and minimum base constraints
515
+ # If violating, adjust size upward to the smallest valid amount respecting size tick
516
+ if min_quote_amount > 0:
517
+ notional = d_price * d_size
518
+ if notional < min_quote_amount:
519
+ # required size to reach min notional
520
+ required = (min_quote_amount / d_price).quantize(quant_size, rounding=ROUND_HALF_UP)
521
+ if required > d_size:
522
+ d_size = required
523
+ if min_base_amount > 0 and d_size < min_base_amount:
524
+ d_size = min_base_amount.quantize(quant_size, rounding=ROUND_HALF_UP)
525
+
526
+ # Respect optional maximum notional limit if provided (>0)
527
+ if order_quote_limit and order_quote_limit > 0:
528
+ notional = d_price * d_size
529
+ if notional > order_quote_limit:
530
+ # Reduce size down to the maximum allowed notional (floor to tick)
531
+ max_size = (order_quote_limit / d_price).quantize(quant_size, rounding=ROUND_DOWN)
532
+ if max_size <= 0:
533
+ raise ValueError("order would exceed order_quote_limit and cannot be reduced to a positive size")
534
+ d_size = max_size
535
+
536
+ # Convert to integer representation expected by signer
461
537
  price_scale = 10 ** int(price_decimals)
462
538
  size_scale = 10 ** int(size_decimals)
463
539
 
464
- price_int = int(round(float(price) * price_scale))
465
- base_amount_int = int(round(float(base_amount) * size_scale))
540
+ price_int = int((d_price * price_scale).to_integral_value(rounding=ROUND_HALF_UP))
541
+ base_amount_int = int((d_size * size_scale).to_integral_value(rounding=ROUND_HALF_UP))
542
+
466
543
  trigger_price_int = (
467
- int(round(float(trigger_price) * price_scale))
544
+ int((Decimal(str(trigger_price)) * price_scale).to_integral_value(rounding=ROUND_HALF_UP))
468
545
  if trigger_price is not None
469
546
  else self.signer.NIL_TRIGGER_PRICE
470
547
  )
@@ -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
 
@@ -632,6 +688,9 @@ class LighterDataStore(DataStoreCollection):
632
688
  elif msg_type in {"subscribed/account_all", "update/account_all"}:
633
689
  self.accounts._on_message(msg)
634
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)
635
694
  elif msg_type in {"subscribed/trade", "update/trade"}:
636
695
  self.klines._on_message(msg)
637
696
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 1.24
3
+ Version: 1.26
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
@@ -17,7 +17,7 @@ Requires-Dist: aiohttp>=3.10.4
17
17
  Requires-Dist: colorama>=0.4.6
18
18
  Requires-Dist: cryptography>=44.0.2
19
19
  Requires-Dist: duckdb>=1.2.2
20
- Requires-Dist: lighter-sdk>=0.1.4
20
+ Requires-Dist: lighter-sdk
21
21
  Requires-Dist: numpy>=1.21.0
22
22
  Requires-Dist: pandas>=2.2.3
23
23
  Requires-Dist: pybotters>=1.9.1
@@ -11,7 +11,7 @@ hyperquant/broker/coinw.py,sha256=SnJU0vASh77rfcpMGWaIfTblQSjQk3vjlW_4juYdbcs,17
11
11
  hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
12
12
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
13
13
  hyperquant/broker/lbank.py,sha256=98M5wmSoeHwbBYMA3rh25zqLb6fQKVaEmwqALF5nOvY,22181
14
- hyperquant/broker/lighter.py,sha256=mgkdHjXXmOAhVlU8uZJ28fcPqzHM0fnjJXXYMTD4wJs,21918
14
+ hyperquant/broker/lighter.py,sha256=xyF967a-AwzecJ9qRPatxEzqis-J8dKQB6Xh1deR9Vc,25611
15
15
  hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
16
16
  hyperquant/broker/ws.py,sha256=AzyFAHIDF4exxwm_IAEV6ihftwAlu19al8Vla4ygk-A,4354
17
17
  hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
@@ -24,12 +24,12 @@ hyperquant/broker/models/coinw.py,sha256=LvLMVP7i-qkkTK1ubw8eBkMK2RQmFoKPxdKqmC4
24
24
  hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
25
25
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
26
26
  hyperquant/broker/models/lbank.py,sha256=vHkNKxIMzpoC_EwcZnEOPOupizF92yGWi9GKxvYYFUQ,19181
27
- hyperquant/broker/models/lighter.py,sha256=vpo1xm3S_FEV3GOlbN9_qEhrgj8qXM7jP8jstBP2S0Y,28217
27
+ hyperquant/broker/models/lighter.py,sha256=I6hjM0of8NLtjSmI6OlbJlvpYDDswLn9yyQLz0I1Mes,30495
28
28
  hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
29
29
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
30
30
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
31
31
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
32
32
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
33
- hyperquant-1.24.dist-info/METADATA,sha256=B2YHKuabMfGfMSx-OVQnCphVOPL5K94nuG5m5PENKAs,4352
34
- hyperquant-1.24.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
- hyperquant-1.24.dist-info/RECORD,,
33
+ hyperquant-1.26.dist-info/METADATA,sha256=MwZeeyWfKbippluTm-wo6qMk1GJ185ZLtX8sZ2BoDTc,4345
34
+ hyperquant-1.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
35
+ hyperquant-1.26.dist-info/RECORD,,