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.
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
|
|
@@ -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
|
-
|
|
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
|
|
310
|
+
"""Subscribe to order updates via Account All Orders stream.
|
|
310
311
|
|
|
311
|
-
|
|
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
|
-
|
|
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(
|
|
465
|
-
base_amount_int = int(
|
|
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(
|
|
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.
|
|
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
|
|
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=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=
|
|
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.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,,
|
|
File without changes
|