hyperquant 1.24__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.

@@ -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
@@ -302,13 +303,39 @@ class Lighter:
302
303
  await ws_app._event.wait()
303
304
  return ws_app
304
305
 
305
- async def sub_accounts(
306
+
307
+ async def sub_orders(
306
308
  self,
307
- account_ids: Sequence[int] | int,
309
+ account_ids: Sequence[int] | int = None,
308
310
  ) -> pybotters.ws.WebSocketApp:
309
- """Subscribe to account-only websocket updates."""
311
+ """Subscribe to order updates via Account All Orders stream.
310
312
 
311
- return await self.sub_orderbook(market_ids=[], account_ids=account_ids)
313
+ Channel per docs: "account_all_orders/{ACCOUNT_ID}" (requires auth).
314
+ Response carries an "orders" mapping of market_id -> [Order].
315
+ """
316
+ if account_ids:
317
+ if isinstance(account_ids, int):
318
+ account_id_list = [str(account_ids)]
319
+ else:
320
+ account_id_list = [str(aid) for aid in account_ids]
321
+ else:
322
+ account_id_list = [self.account_index]
323
+
324
+ channels = [f"account_all_orders/{aid}" for aid in account_id_list]
325
+ send_payload = [
326
+ {"type": "subscribe", "channel": channel, "auth": self.auth}
327
+ for channel in channels
328
+ ]
329
+
330
+ ws_app = self.client.ws_connect(
331
+ self.ws_url,
332
+ send_json=send_payload,
333
+ hdlr_json=self.store.onmessage,
334
+ )
335
+ await ws_app._event.wait()
336
+ return ws_app
337
+
338
+
312
339
 
313
340
  async def sub_kline(
314
341
  self,
@@ -441,30 +468,81 @@ class Lighter:
441
468
  except KeyError as exc:
442
469
  raise ValueError(f"Unsupported time_in_force: {time_in_force}") from exc
443
470
 
444
- expiry = order_expiry if order_expiry is not None else self.signer.DEFAULT_28_DAY_ORDER_EXPIRY
471
+ # Per WS/API docs, OrderExpiry can be 0 with ExpiredAt computed by signer.
472
+ # Use caller-provided value if given; otherwise default to 0 to avoid
473
+ # "OrderExpiry is invalid" errors on some markets.
474
+ expiry = order_expiry if order_expiry is not None else 0
445
475
  nonce_value = nonce if nonce is not None else -1
446
476
  api_key_idx = api_key_index if api_key_index is not None else self.api_key_index
447
477
 
478
+ # ----- Precision and min constraints handling -----
479
+ # Prefer explicitly supported decimals. Avoid using quote decimals to infer size.
448
480
  price_decimals = (
449
481
  detail.get("supported_price_decimals")
450
482
  or detail.get("price_decimals")
451
- or detail.get("quote_decimals")
452
483
  or 0
453
484
  )
454
485
  size_decimals = (
455
486
  detail.get("supported_size_decimals")
456
487
  or detail.get("size_decimals")
457
- or detail.get("supported_quote_decimals")
458
488
  or 0
459
489
  )
460
490
 
491
+ # Optional constraints provided by the API
492
+ # Strings like "10.000000" may be returned – normalize via Decimal for accuracy
493
+ def _to_decimal(v, default: str | int = 0):
494
+ try:
495
+ if v is None or v == "":
496
+ return Decimal(str(default))
497
+ return Decimal(str(v))
498
+ except Exception:
499
+ return Decimal(str(default))
500
+
501
+ min_base_amount = _to_decimal(detail.get("min_base_amount"), 0)
502
+ min_quote_amount = _to_decimal(detail.get("min_quote_amount"), 0)
503
+ order_quote_limit = _to_decimal(detail.get("order_quote_limit"), 0)
504
+
505
+ # Use Decimal for precise arithmetic and quantization
506
+ d_price = Decimal(str(price))
507
+ d_size = Decimal(str(base_amount))
508
+ quant_price = Decimal(1) / (Decimal(10) ** int(price_decimals)) if int(price_decimals) > 0 else Decimal(1)
509
+ quant_size = Decimal(1) / (Decimal(10) ** int(size_decimals)) if int(size_decimals) > 0 else Decimal(1)
510
+
511
+ # Round price/size to allowed decimals (half up to the nearest tick)
512
+ d_price = d_price.quantize(quant_price, rounding=ROUND_HALF_UP)
513
+ d_size = d_size.quantize(quant_size, rounding=ROUND_HALF_UP)
514
+
515
+ # Ensure minimum notional and minimum base constraints
516
+ # If violating, adjust size upward to the smallest valid amount respecting size tick
517
+ if min_quote_amount > 0:
518
+ notional = d_price * d_size
519
+ if notional < min_quote_amount:
520
+ # required size to reach min notional
521
+ required = (min_quote_amount / d_price).quantize(quant_size, rounding=ROUND_HALF_UP)
522
+ if required > d_size:
523
+ d_size = required
524
+ if min_base_amount > 0 and d_size < min_base_amount:
525
+ d_size = min_base_amount.quantize(quant_size, rounding=ROUND_HALF_UP)
526
+
527
+ # Respect optional maximum notional limit if provided (>0)
528
+ if order_quote_limit and order_quote_limit > 0:
529
+ notional = d_price * d_size
530
+ if notional > order_quote_limit:
531
+ # Reduce size down to the maximum allowed notional (floor to tick)
532
+ max_size = (order_quote_limit / d_price).quantize(quant_size, rounding=ROUND_DOWN)
533
+ if max_size <= 0:
534
+ raise ValueError("order would exceed order_quote_limit and cannot be reduced to a positive size")
535
+ d_size = max_size
536
+
537
+ # Convert to integer representation expected by signer
461
538
  price_scale = 10 ** int(price_decimals)
462
539
  size_scale = 10 ** int(size_decimals)
463
540
 
464
- price_int = int(round(float(price) * price_scale))
465
- base_amount_int = int(round(float(base_amount) * size_scale))
541
+ price_int = int((d_price * price_scale).to_integral_value(rounding=ROUND_HALF_UP))
542
+ base_amount_int = int((d_size * size_scale).to_integral_value(rounding=ROUND_HALF_UP))
543
+
466
544
  trigger_price_int = (
467
- int(round(float(trigger_price) * price_scale))
545
+ int((Decimal(str(trigger_price)) * price_scale).to_integral_value(rounding=ROUND_HALF_UP))
468
546
  if trigger_price is not None
469
547
  else self.signer.NIL_TRIGGER_PRICE
470
548
  )
@@ -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.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
@@ -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=YuMR6pCWhJjZ2tGMbYanATvbV6t1J22-pyHqO1GNr2g,25639
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.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,,