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.
hyperquant/broker/lighter.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
311
|
+
"""Subscribe to order updates via Account All Orders stream.
|
|
310
312
|
|
|
311
|
-
|
|
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
|
-
|
|
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(
|
|
465
|
-
base_amount_int = int(
|
|
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(
|
|
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.
|
|
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
|
|
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=
|
|
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=
|
|
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.
|
|
34
|
-
hyperquant-1.
|
|
35
|
-
hyperquant-1.
|
|
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,,
|
|
File without changes
|