hyperquant 1.22__py3-none-any.whl → 1.24__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 +134 -2
- hyperquant/broker/models/lighter.py +301 -0
- hyperquant/broker/ws.py +7 -0
- {hyperquant-1.22.dist-info → hyperquant-1.24.dist-info}/METADATA +2 -5
- {hyperquant-1.22.dist-info → hyperquant-1.24.dist-info}/RECORD +6 -8
- hyperquant/broker/coinup.py +0 -591
- hyperquant/broker/models/coinup.py +0 -334
- {hyperquant-1.22.dist-info → hyperquant-1.24.dist-info}/WHEEL +0 -0
hyperquant/broker/lighter.py
CHANGED
|
@@ -9,6 +9,7 @@ import pybotters
|
|
|
9
9
|
|
|
10
10
|
from lighter.api.account_api import AccountApi
|
|
11
11
|
from lighter.api.order_api import OrderApi
|
|
12
|
+
from lighter.api.candlestick_api import CandlestickApi
|
|
12
13
|
from lighter.api_client import ApiClient
|
|
13
14
|
from lighter.configuration import Configuration
|
|
14
15
|
from lighter.signer_client import SignerClient
|
|
@@ -31,6 +32,7 @@ class Lighter:
|
|
|
31
32
|
api_key_index: int = 3,
|
|
32
33
|
api_client: ApiClient | None = None,
|
|
33
34
|
order_api: OrderApi | None = None,
|
|
35
|
+
candlestick_api: CandlestickApi | None = None,
|
|
34
36
|
account_api: AccountApi | None = None,
|
|
35
37
|
ws_url: str | None = None,
|
|
36
38
|
) -> None:
|
|
@@ -46,6 +48,7 @@ class Lighter:
|
|
|
46
48
|
self._owns_api_client = api_client is None
|
|
47
49
|
|
|
48
50
|
self.order_api = order_api or OrderApi(self._api_client)
|
|
51
|
+
self.candlestick_api = candlestick_api or CandlestickApi(self._api_client)
|
|
49
52
|
self.account_api = account_api or AccountApi(self._api_client)
|
|
50
53
|
self.signer: SignerClient = None
|
|
51
54
|
|
|
@@ -150,7 +153,9 @@ class Lighter:
|
|
|
150
153
|
|
|
151
154
|
|
|
152
155
|
if include_detail:
|
|
153
|
-
|
|
156
|
+
# Use raw HTTP to avoid strict SDK model validation issues (e.g., status 'inactive').
|
|
157
|
+
url = f"{self.configuration.host.rstrip('/')}/api/v1/orderBooks"
|
|
158
|
+
tasks.append(("detail", self.client.get(url)))
|
|
154
159
|
|
|
155
160
|
if include_orders:
|
|
156
161
|
if account_index is None or symbol is None:
|
|
@@ -206,7 +211,12 @@ class Lighter:
|
|
|
206
211
|
results: dict[str, Any] = {}
|
|
207
212
|
for key, coroutine in tasks:
|
|
208
213
|
try:
|
|
209
|
-
|
|
214
|
+
resp = await coroutine
|
|
215
|
+
if key == "detail":
|
|
216
|
+
# Parse JSON body for detail endpoint
|
|
217
|
+
results[key] = await resp.json()
|
|
218
|
+
else:
|
|
219
|
+
results[key] = resp
|
|
210
220
|
except Exception:
|
|
211
221
|
logger.exception("Lighter REST request %s failed", key)
|
|
212
222
|
raise
|
|
@@ -300,6 +310,71 @@ class Lighter:
|
|
|
300
310
|
|
|
301
311
|
return await self.sub_orderbook(market_ids=[], account_ids=account_ids)
|
|
302
312
|
|
|
313
|
+
async def sub_kline(
|
|
314
|
+
self,
|
|
315
|
+
symbols: Sequence[str] | str,
|
|
316
|
+
*,
|
|
317
|
+
resolutions: Sequence[str] | str,
|
|
318
|
+
) -> pybotters.ws.WebSocketApp:
|
|
319
|
+
"""Subscribe to trade streams and aggregate into klines in the store.
|
|
320
|
+
|
|
321
|
+
- symbols: list of symbols (e.g., ["BTC-USD"]) or a single symbol; may also be numeric market_ids.
|
|
322
|
+
- resolutions: list like ["1m", "5m"] or a single resolution; added to kline store for aggregation.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
# Normalize inputs
|
|
326
|
+
symbol_list = [symbols] if isinstance(symbols, str) else list(symbols)
|
|
327
|
+
res_list = [resolutions] if isinstance(resolutions, str) else list(resolutions)
|
|
328
|
+
|
|
329
|
+
if not symbol_list:
|
|
330
|
+
raise ValueError("At least one symbol must be provided")
|
|
331
|
+
if not res_list:
|
|
332
|
+
raise ValueError("At least one resolution must be provided")
|
|
333
|
+
|
|
334
|
+
# Ensure market metadata for symbol->market_id resolution
|
|
335
|
+
needs_detail = any(self.get_contract_id(sym) is None and not str(sym).isdigit() for sym in symbol_list)
|
|
336
|
+
if needs_detail:
|
|
337
|
+
try:
|
|
338
|
+
await self.update("detail")
|
|
339
|
+
except Exception:
|
|
340
|
+
logger.exception("Failed to refresh Lighter market metadata for kline subscription")
|
|
341
|
+
raise
|
|
342
|
+
|
|
343
|
+
# Resolve market ids and populate id->symbol mapping for klines store
|
|
344
|
+
trade_market_ids: list[str] = []
|
|
345
|
+
for sym in symbol_list:
|
|
346
|
+
market_id = self.get_contract_id(sym)
|
|
347
|
+
if market_id is None:
|
|
348
|
+
if str(sym).isdigit():
|
|
349
|
+
market_id = str(sym)
|
|
350
|
+
symbol_for_map = str(sym)
|
|
351
|
+
else:
|
|
352
|
+
raise ValueError(f"Unknown symbol: {sym}")
|
|
353
|
+
else:
|
|
354
|
+
symbol_for_map = sym
|
|
355
|
+
market_id_str = str(market_id)
|
|
356
|
+
trade_market_ids.append(market_id_str)
|
|
357
|
+
# ensure klines store can resolve symbol from market id
|
|
358
|
+
self.store.klines.id_to_symbol[market_id_str] = symbol_for_map
|
|
359
|
+
|
|
360
|
+
# Register resolutions into kline store aggregation list
|
|
361
|
+
for r in res_list:
|
|
362
|
+
if r not in self.store.klines._res_list:
|
|
363
|
+
self.store.klines._res_list.append(r)
|
|
364
|
+
|
|
365
|
+
# Build subscribe payload for trade channels
|
|
366
|
+
channels = [f"trade/{mid}" for mid in trade_market_ids]
|
|
367
|
+
send_payload = [{"type": "subscribe", "channel": ch} for ch in channels]
|
|
368
|
+
|
|
369
|
+
ws_app = self.client.ws_connect(
|
|
370
|
+
self.ws_url,
|
|
371
|
+
send_json=send_payload,
|
|
372
|
+
hdlr_json=self.store.onmessage,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
await ws_app._event.wait()
|
|
376
|
+
return ws_app
|
|
377
|
+
|
|
303
378
|
async def place_order(
|
|
304
379
|
self,
|
|
305
380
|
symbol: str,
|
|
@@ -468,3 +543,60 @@ class Lighter:
|
|
|
468
543
|
"request": request_payload,
|
|
469
544
|
"response": response_payload,
|
|
470
545
|
}
|
|
546
|
+
|
|
547
|
+
async def update_kline(
|
|
548
|
+
self,
|
|
549
|
+
symbol: str,
|
|
550
|
+
*,
|
|
551
|
+
resolution: str,
|
|
552
|
+
start_timestamp: int,
|
|
553
|
+
end_timestamp: int,
|
|
554
|
+
count_back: int,
|
|
555
|
+
set_timestamp_to_end: bool | None = None,
|
|
556
|
+
) -> list[dict[str, Any]]:
|
|
557
|
+
"""Fetch candlesticks and update the Kline store.
|
|
558
|
+
|
|
559
|
+
Parameters
|
|
560
|
+
- symbol: market symbol, e.g. "BTC-USD".
|
|
561
|
+
- resolution: e.g. "1m", "5m", "1h".
|
|
562
|
+
- start_timestamp: epoch milliseconds.
|
|
563
|
+
- end_timestamp: epoch milliseconds.
|
|
564
|
+
- count_back: number of bars to fetch.
|
|
565
|
+
- set_timestamp_to_end: if True, API sets last bar timestamp to the end.
|
|
566
|
+
"""
|
|
567
|
+
|
|
568
|
+
market_id = self.get_contract_id(symbol)
|
|
569
|
+
if market_id is None:
|
|
570
|
+
# try to refresh metadata once
|
|
571
|
+
await self.update("detail")
|
|
572
|
+
market_id = self.get_contract_id(symbol)
|
|
573
|
+
if market_id is None:
|
|
574
|
+
raise ValueError(f"Unknown symbol: {symbol}")
|
|
575
|
+
|
|
576
|
+
resp = await self.candlestick_api.candlesticks(
|
|
577
|
+
market_id=int(market_id),
|
|
578
|
+
resolution=resolution,
|
|
579
|
+
start_timestamp=int(start_timestamp),
|
|
580
|
+
end_timestamp=int(end_timestamp),
|
|
581
|
+
count_back=int(count_back),
|
|
582
|
+
set_timestamp_to_end=bool(set_timestamp_to_end) if set_timestamp_to_end is not None else None,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# Update store
|
|
586
|
+
self.store.klines._onresponse(resp, symbol=symbol, resolution=resolution)
|
|
587
|
+
|
|
588
|
+
payload = _maybe_to_dict(resp) or {}
|
|
589
|
+
items = payload.get("candlesticks") or []
|
|
590
|
+
# attach symbol/resolution to return
|
|
591
|
+
out: list[dict[str, Any]] = []
|
|
592
|
+
for it in items:
|
|
593
|
+
if hasattr(it, "to_dict"):
|
|
594
|
+
d = it.to_dict()
|
|
595
|
+
elif hasattr(it, "model_dump"):
|
|
596
|
+
d = it.model_dump()
|
|
597
|
+
else:
|
|
598
|
+
d = dict(it) if isinstance(it, dict) else {"value": it}
|
|
599
|
+
d["symbol"] = symbol
|
|
600
|
+
d["resolution"] = resolution
|
|
601
|
+
out.append(d)
|
|
602
|
+
return out
|
|
@@ -324,6 +324,282 @@ class Positions(DataStore):
|
|
|
324
324
|
self._update_positions(account_index, positions)
|
|
325
325
|
|
|
326
326
|
|
|
327
|
+
class Klines(DataStore):
|
|
328
|
+
"""Candlestick/Kline store keyed by (symbol, resolution, timestamp).
|
|
329
|
+
|
|
330
|
+
- Maintains a list of active resolutions in ``_res_list`` (populated by REST updates).
|
|
331
|
+
- Updates candles in real-time by aggregating trade websocket messages.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
_KEYS = ["symbol", "resolution", "timestamp"]
|
|
335
|
+
|
|
336
|
+
def _init(self) -> None:
|
|
337
|
+
self.id_to_symbol: dict[str, str] = {}
|
|
338
|
+
self._current_symbol: str | None = None
|
|
339
|
+
self._res_list: list[str] = []
|
|
340
|
+
# Track last processed trade_id to deduplicate snapshot trades after reconnect
|
|
341
|
+
self._last_trade_id_by_market: dict[str, int] = {}
|
|
342
|
+
self._last_trade_id_by_symbol: dict[str, int] = {}
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _resolution_to_ms(resolution: str) -> int | None:
|
|
346
|
+
try:
|
|
347
|
+
res = resolution.strip().lower()
|
|
348
|
+
except Exception:
|
|
349
|
+
return None
|
|
350
|
+
# Common forms: 1m, 5m, 1h, 1d; also allow pure digits => seconds
|
|
351
|
+
unit = res[-1]
|
|
352
|
+
num_part = res[:-1] if unit in {"s", "m", "h", "d", "w"} else res
|
|
353
|
+
try:
|
|
354
|
+
n = int(num_part)
|
|
355
|
+
except Exception:
|
|
356
|
+
return None
|
|
357
|
+
if unit == "s":
|
|
358
|
+
return n * 1000
|
|
359
|
+
if unit == "m" or unit not in {"s", "h", "d", "w"}: # default minutes if no unit
|
|
360
|
+
return n * 60 * 1000
|
|
361
|
+
if unit == "h":
|
|
362
|
+
return n * 60 * 60 * 1000
|
|
363
|
+
if unit == "d":
|
|
364
|
+
return n * 24 * 60 * 60 * 1000
|
|
365
|
+
if unit == "w":
|
|
366
|
+
return n * 7 * 24 * 60 * 60 * 1000
|
|
367
|
+
return None
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _market_id_from_channel(channel: str | None) -> str | None:
|
|
371
|
+
if not channel:
|
|
372
|
+
return None
|
|
373
|
+
if ":" in channel:
|
|
374
|
+
return channel.split(":", 1)[1]
|
|
375
|
+
if "/" in channel:
|
|
376
|
+
return channel.split("/", 1)[1]
|
|
377
|
+
return channel
|
|
378
|
+
|
|
379
|
+
def _compose_item(
|
|
380
|
+
self,
|
|
381
|
+
*,
|
|
382
|
+
symbol: str,
|
|
383
|
+
resolution: str,
|
|
384
|
+
ts: int,
|
|
385
|
+
price: float,
|
|
386
|
+
size: float,
|
|
387
|
+
last_trade_id: int | None,
|
|
388
|
+
open_price: float | None = None,
|
|
389
|
+
) -> dict[str, Any]:
|
|
390
|
+
return {
|
|
391
|
+
"symbol": symbol,
|
|
392
|
+
"resolution": resolution,
|
|
393
|
+
"timestamp": ts,
|
|
394
|
+
"open": price if open_price is None else float(open_price),
|
|
395
|
+
"high": price,
|
|
396
|
+
"low": price,
|
|
397
|
+
"close": price,
|
|
398
|
+
"volume0": abs(size),
|
|
399
|
+
"volume1": abs(size) * price,
|
|
400
|
+
"last_trade_id": last_trade_id or 0,
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
def _ensure_backfill(self, *, symbol: str, resolution: str, new_bucket_ts: int) -> None:
|
|
404
|
+
"""Backfill missing empty bars up to (but not including) new_bucket_ts.
|
|
405
|
+
|
|
406
|
+
Uses the last known close as O/H/L/C for synthetic bars and zero volume.
|
|
407
|
+
"""
|
|
408
|
+
step = self._resolution_to_ms(resolution)
|
|
409
|
+
if not step:
|
|
410
|
+
return
|
|
411
|
+
# find the last existing bar before new_bucket_ts
|
|
412
|
+
rows = self.find({"symbol": symbol, "resolution": resolution})
|
|
413
|
+
prev = None
|
|
414
|
+
prev_ts = None
|
|
415
|
+
for r in rows:
|
|
416
|
+
try:
|
|
417
|
+
ts = int(r.get("timestamp"))
|
|
418
|
+
except Exception:
|
|
419
|
+
continue
|
|
420
|
+
if ts < new_bucket_ts and (prev_ts is None or ts > prev_ts):
|
|
421
|
+
prev = r
|
|
422
|
+
prev_ts = ts
|
|
423
|
+
if prev is None or prev_ts is None:
|
|
424
|
+
return
|
|
425
|
+
expected = prev_ts + step
|
|
426
|
+
while expected < new_bucket_ts:
|
|
427
|
+
prev_close = float(prev.get("close"))
|
|
428
|
+
fill_item = {
|
|
429
|
+
"symbol": symbol,
|
|
430
|
+
"resolution": resolution,
|
|
431
|
+
"timestamp": expected,
|
|
432
|
+
"open": prev_close,
|
|
433
|
+
"high": prev_close,
|
|
434
|
+
"low": prev_close,
|
|
435
|
+
"close": prev_close,
|
|
436
|
+
"volume0": 0.0,
|
|
437
|
+
"volume1": 0.0,
|
|
438
|
+
"last_trade_id": int(prev.get("last_trade_id", 0)) if prev.get("last_trade_id") is not None else 0,
|
|
439
|
+
}
|
|
440
|
+
self._insert([fill_item])
|
|
441
|
+
prev = fill_item
|
|
442
|
+
expected += step
|
|
443
|
+
|
|
444
|
+
def _merge_trade(self, *, symbol: str, trade_ts_ms: int, price: float, size: float, last_trade_id: int | None) -> None:
|
|
445
|
+
# Iterate active resolutions
|
|
446
|
+
for res in list(self._res_list):
|
|
447
|
+
interval_ms = self._resolution_to_ms(res)
|
|
448
|
+
if not interval_ms:
|
|
449
|
+
continue
|
|
450
|
+
bucket_ts = (trade_ts_ms // interval_ms) * interval_ms
|
|
451
|
+
# Upsert logic
|
|
452
|
+
existing = self.get({"symbol": symbol, "resolution": res, "timestamp": bucket_ts})
|
|
453
|
+
if existing is None:
|
|
454
|
+
# backfill any missing empty bars before creating a new bucket
|
|
455
|
+
self._ensure_backfill(symbol=symbol, resolution=res, new_bucket_ts=bucket_ts)
|
|
456
|
+
# open should be previous bar's close if exists; if none, fall back to current price
|
|
457
|
+
prev = None
|
|
458
|
+
rows = self.find({"symbol": symbol, "resolution": res})
|
|
459
|
+
prev_ts = None
|
|
460
|
+
for r in rows:
|
|
461
|
+
try:
|
|
462
|
+
ts = int(r.get("timestamp"))
|
|
463
|
+
except Exception:
|
|
464
|
+
continue
|
|
465
|
+
if ts < bucket_ts and (prev_ts is None or ts > prev_ts):
|
|
466
|
+
prev = r
|
|
467
|
+
prev_ts = ts
|
|
468
|
+
open_px = float(prev.get("close")) if prev is not None else price
|
|
469
|
+
self._insert([
|
|
470
|
+
self._compose_item(
|
|
471
|
+
symbol=symbol,
|
|
472
|
+
resolution=res,
|
|
473
|
+
ts=bucket_ts,
|
|
474
|
+
price=price,
|
|
475
|
+
size=size,
|
|
476
|
+
last_trade_id=last_trade_id,
|
|
477
|
+
open_price=open_px,
|
|
478
|
+
)
|
|
479
|
+
])
|
|
480
|
+
continue
|
|
481
|
+
# merge into existing
|
|
482
|
+
updated = dict(existing)
|
|
483
|
+
o = float(updated.get("open", price))
|
|
484
|
+
h = float(updated.get("high", price))
|
|
485
|
+
l = float(updated.get("low", price))
|
|
486
|
+
c = float(updated.get("close", price))
|
|
487
|
+
v0 = float(updated.get("volume0", 0.0))
|
|
488
|
+
v1 = float(updated.get("volume1", 0.0))
|
|
489
|
+
p = float(price)
|
|
490
|
+
s = abs(float(size))
|
|
491
|
+
updated["open"] = o
|
|
492
|
+
updated["high"] = max(h, p)
|
|
493
|
+
updated["low"] = min(l, p)
|
|
494
|
+
updated["close"] = p
|
|
495
|
+
updated["volume0"] = v0 + s
|
|
496
|
+
updated["volume1"] = v1 + s * p
|
|
497
|
+
if last_trade_id is not None:
|
|
498
|
+
try:
|
|
499
|
+
updated["last_trade_id"] = max(int(last_trade_id), int(updated.get("last_trade_id", 0)))
|
|
500
|
+
except Exception:
|
|
501
|
+
updated["last_trade_id"] = int(last_trade_id)
|
|
502
|
+
self._update([updated])
|
|
503
|
+
|
|
504
|
+
def _onresponse(self, data: Any, *, symbol: str | None = None, resolution: str | None = None) -> None:
|
|
505
|
+
payload = _maybe_to_dict(data) or {}
|
|
506
|
+
candlesticks = payload.get("candlesticks") or []
|
|
507
|
+
res = payload.get("resolution") or resolution
|
|
508
|
+
if res not in self._res_list and res is not None:
|
|
509
|
+
self._res_list.append(res)
|
|
510
|
+
|
|
511
|
+
sym = symbol or self._current_symbol
|
|
512
|
+
|
|
513
|
+
# Sort incoming bars by timestamp to backfill in order
|
|
514
|
+
items: list[dict[str, Any]] = []
|
|
515
|
+
for c in sorted((candlesticks or []), key=lambda x: x.get("timestamp", 0)):
|
|
516
|
+
if not isinstance(c, dict):
|
|
517
|
+
continue
|
|
518
|
+
entry = dict(c)
|
|
519
|
+
if sym is not None:
|
|
520
|
+
entry["symbol"] = sym
|
|
521
|
+
if res is not None:
|
|
522
|
+
entry["resolution"] = res
|
|
523
|
+
items.append(entry)
|
|
524
|
+
|
|
525
|
+
# Insert or update per bar; backfill gaps before inserting new bars
|
|
526
|
+
for entry in items:
|
|
527
|
+
sym_i = entry.get("symbol")
|
|
528
|
+
res_i = entry.get("resolution")
|
|
529
|
+
ts_i = entry.get("timestamp")
|
|
530
|
+
if sym_i is None or res_i is None or ts_i is None:
|
|
531
|
+
continue
|
|
532
|
+
if self.get({"symbol": sym_i, "resolution": res_i, "timestamp": ts_i}) is None:
|
|
533
|
+
self._ensure_backfill(symbol=sym_i, resolution=res_i, new_bucket_ts=int(ts_i))
|
|
534
|
+
self._insert([entry])
|
|
535
|
+
else:
|
|
536
|
+
self._update([entry])
|
|
537
|
+
|
|
538
|
+
# Update last_trade_id baseline (by symbol) from REST bars if available
|
|
539
|
+
if sym is not None:
|
|
540
|
+
max_tid = 0
|
|
541
|
+
for e in items:
|
|
542
|
+
try:
|
|
543
|
+
tid = int(e.get("last_trade_id", 0))
|
|
544
|
+
except Exception:
|
|
545
|
+
tid = 0
|
|
546
|
+
if tid > max_tid:
|
|
547
|
+
max_tid = tid
|
|
548
|
+
if max_tid:
|
|
549
|
+
prev = self._last_trade_id_by_symbol.get(sym, 0)
|
|
550
|
+
if max_tid > prev:
|
|
551
|
+
self._last_trade_id_by_symbol[sym] = max_tid
|
|
552
|
+
|
|
553
|
+
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
554
|
+
msg_type = msg.get("type")
|
|
555
|
+
if msg_type not in {"subscribed/trade", "update/trade"}:
|
|
556
|
+
return
|
|
557
|
+
market_id = self._market_id_from_channel(msg.get("channel"))
|
|
558
|
+
if market_id is None:
|
|
559
|
+
return
|
|
560
|
+
market_id_str = str(market_id)
|
|
561
|
+
symbol = self.id_to_symbol.get(market_id_str) or market_id_str
|
|
562
|
+
trades = msg.get("trades") or []
|
|
563
|
+
# Baseline last trade_id from market and symbol
|
|
564
|
+
base_last_tid = max(
|
|
565
|
+
self._last_trade_id_by_market.get(market_id_str, 0),
|
|
566
|
+
self._last_trade_id_by_symbol.get(symbol, 0),
|
|
567
|
+
)
|
|
568
|
+
# Process in ascending trade_id order for stability
|
|
569
|
+
try:
|
|
570
|
+
trades_sorted = sorted(trades, key=lambda x: int(x.get("trade_id", 0)))
|
|
571
|
+
except Exception:
|
|
572
|
+
trades_sorted = trades
|
|
573
|
+
|
|
574
|
+
last_tid = base_last_tid
|
|
575
|
+
for t in trades_sorted:
|
|
576
|
+
if not isinstance(t, dict):
|
|
577
|
+
continue
|
|
578
|
+
ts = t.get("timestamp")
|
|
579
|
+
price = t.get("price")
|
|
580
|
+
size = t.get("size")
|
|
581
|
+
trade_id = t.get("trade_id")
|
|
582
|
+
try:
|
|
583
|
+
ts = int(ts)
|
|
584
|
+
p = float(price)
|
|
585
|
+
s = float(size)
|
|
586
|
+
tid = int(trade_id) if trade_id is not None else 0
|
|
587
|
+
except Exception:
|
|
588
|
+
continue
|
|
589
|
+
# Skip stale or duplicate snapshot trades
|
|
590
|
+
if tid and last_tid and tid <= last_tid:
|
|
591
|
+
continue
|
|
592
|
+
self._merge_trade(symbol=symbol, trade_ts_ms=ts, price=p, size=s, last_trade_id=tid)
|
|
593
|
+
if tid > last_tid:
|
|
594
|
+
last_tid = tid
|
|
595
|
+
|
|
596
|
+
# Persist last processed trade_id for this market
|
|
597
|
+
if last_tid and last_tid > base_last_tid:
|
|
598
|
+
self._last_trade_id_by_market[market_id_str] = last_tid
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
|
|
327
603
|
class LighterDataStore(DataStoreCollection):
|
|
328
604
|
"""Data store collection for the Lighter exchange."""
|
|
329
605
|
|
|
@@ -333,12 +609,14 @@ class LighterDataStore(DataStoreCollection):
|
|
|
333
609
|
self._create("orders", datastore_class=Orders)
|
|
334
610
|
self._create("accounts", datastore_class=Accounts)
|
|
335
611
|
self._create("positions", datastore_class=Positions)
|
|
612
|
+
self._create("klines", datastore_class=Klines)
|
|
336
613
|
|
|
337
614
|
def set_id_to_symbol(self, id_to_symbol: dict[str, str]) -> None:
|
|
338
615
|
self.id_to_symbol = id_to_symbol
|
|
339
616
|
self.book.id_to_symbol = self.id_to_symbol
|
|
340
617
|
self.orders.id_to_symbol = self.id_to_symbol
|
|
341
618
|
self.positions.id_to_symbol = self.id_to_symbol
|
|
619
|
+
self.klines.id_to_symbol = self.id_to_symbol
|
|
342
620
|
|
|
343
621
|
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
344
622
|
|
|
@@ -354,6 +632,8 @@ class LighterDataStore(DataStoreCollection):
|
|
|
354
632
|
elif msg_type in {"subscribed/account_all", "update/account_all"}:
|
|
355
633
|
self.accounts._on_message(msg)
|
|
356
634
|
self.positions._on_message(msg)
|
|
635
|
+
elif msg_type in {"subscribed/trade", "update/trade"}:
|
|
636
|
+
self.klines._on_message(msg)
|
|
357
637
|
|
|
358
638
|
|
|
359
639
|
@property
|
|
@@ -506,3 +786,24 @@ class LighterDataStore(DataStoreCollection):
|
|
|
506
786
|
]
|
|
507
787
|
"""
|
|
508
788
|
return self._get("positions")
|
|
789
|
+
|
|
790
|
+
@property
|
|
791
|
+
def klines(self) -> "Klines":
|
|
792
|
+
"""
|
|
793
|
+
K线/蜡烛图数据(`lighter.models.Candlesticks` -> `lighter.models.Candlestick`)。
|
|
794
|
+
|
|
795
|
+
.. code:: json
|
|
796
|
+
|
|
797
|
+
{
|
|
798
|
+
"symbol": "BTC",
|
|
799
|
+
"timestamp": 1730612700000,
|
|
800
|
+
"open": 68970.5,
|
|
801
|
+
"high": 69012.3,
|
|
802
|
+
"low": 68890.0,
|
|
803
|
+
"close": 68995.1,
|
|
804
|
+
"volume0": 12.34,
|
|
805
|
+
"volume1": 850000.0,
|
|
806
|
+
"resolution": "1m"
|
|
807
|
+
}
|
|
808
|
+
"""
|
|
809
|
+
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.
|
|
3
|
+
Version: 1.24
|
|
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
|
-
Requires-Dist: lighter-sdk
|
|
22
|
-
Requires-Dist: numba>=0.62.1
|
|
20
|
+
Requires-Dist: lighter-sdk>=0.1.4
|
|
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=
|
|
14
|
+
hyperquant/broker/lighter.py,sha256=mgkdHjXXmOAhVlU8uZJ28fcPqzHM0fnjJXXYMTD4wJs,21918
|
|
16
15
|
hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
|
|
17
|
-
hyperquant/broker/ws.py,sha256=
|
|
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=
|
|
27
|
+
hyperquant/broker/models/lighter.py,sha256=vpo1xm3S_FEV3GOlbN9_qEhrgj8qXM7jP8jstBP2S0Y,28217
|
|
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.
|
|
36
|
-
hyperquant-1.
|
|
37
|
-
hyperquant-1.
|
|
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,,
|