hypertrader 0.1.4__tar.gz → 0.1.6__tar.gz
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.
- {hypertrader-0.1.4 → hypertrader-0.1.6}/PKG-INFO +1 -1
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.egg-info/PKG-INFO +1 -1
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.py +12 -10
- {hypertrader-0.1.4 → hypertrader-0.1.6}/modes/auto_trader.py +15 -5
- {hypertrader-0.1.4 → hypertrader-0.1.6}/modes/position_management.py +55 -15
- {hypertrader-0.1.4 → hypertrader-0.1.6}/modes/position_watcher.py +2 -2
- {hypertrader-0.1.4 → hypertrader-0.1.6}/setup.py +1 -1
- {hypertrader-0.1.4 → hypertrader-0.1.6}/utils/helpers.py +274 -14
- {hypertrader-0.1.4 → hypertrader-0.1.6}/README.md +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.egg-info/SOURCES.txt +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.egg-info/dependency_links.txt +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.egg-info/entry_points.txt +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.egg-info/requires.txt +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/hypertrader.egg-info/top_level.txt +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/modes/market_maker.py +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/modes/trailing_stop.py +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/setup.cfg +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/utils/constants.py +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/utils/style.py +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/utils/timer.py +0 -0
- {hypertrader-0.1.4 → hypertrader-0.1.6}/utils/worker.py +0 -0
|
@@ -23,10 +23,9 @@ import os
|
|
|
23
23
|
import sys
|
|
24
24
|
from typing import Any, Dict, List, Optional
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
from hyperliquid.info import Info
|
|
28
28
|
|
|
29
|
-
from modes.auto_trader import run_auto_trader
|
|
30
29
|
from modes.market_maker import run_market_maker
|
|
31
30
|
from modes.position_management import run_bracket_entry
|
|
32
31
|
from modes.position_watcher import run_position_watcher
|
|
@@ -35,18 +34,16 @@ from utils.constants import DEFAULT_LOG_FILE, INTERVAL_TO_MS
|
|
|
35
34
|
from utils.helpers import parse_fractional_pct
|
|
36
35
|
from utils.style import install_pretty_stdout
|
|
37
36
|
|
|
38
|
-
try:
|
|
39
|
-
import numpy as np
|
|
40
|
-
import talib
|
|
41
|
-
except Exception: # TA-Lib is only required for the auto command.
|
|
42
|
-
np = None # type: ignore[assignment]
|
|
43
|
-
talib = None # type: ignore[assignment]
|
|
44
|
-
|
|
45
37
|
try:
|
|
46
38
|
os.mkdir("logs")
|
|
47
39
|
except FileExistsError:
|
|
48
40
|
pass
|
|
49
41
|
|
|
42
|
+
try:
|
|
43
|
+
import uvloop
|
|
44
|
+
except ImportError:
|
|
45
|
+
uvloop = None
|
|
46
|
+
|
|
50
47
|
decimal.getcontext().prec = 4
|
|
51
48
|
install_pretty_stdout()
|
|
52
49
|
|
|
@@ -525,6 +522,8 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
|
|
|
525
522
|
return
|
|
526
523
|
|
|
527
524
|
if args.command == "auto":
|
|
525
|
+
from modes.auto_trader import run_auto_trader
|
|
526
|
+
|
|
528
527
|
if args.size is not None and args.size <= 0.0:
|
|
529
528
|
print("[ERROR] --size must be > 0.")
|
|
530
529
|
sys.exit(1)
|
|
@@ -744,5 +743,8 @@ def main(argv: Optional[List[str]] = None) -> None:
|
|
|
744
743
|
|
|
745
744
|
|
|
746
745
|
if __name__ == "__main__":
|
|
747
|
-
uvloop
|
|
746
|
+
if uvloop:
|
|
747
|
+
uvloop.install()
|
|
748
|
+
else:
|
|
749
|
+
print('[WARN]: Please install uvloop for increased performance. Reverting to standard event loop.')
|
|
748
750
|
main()
|
|
@@ -5,8 +5,6 @@ import logging
|
|
|
5
5
|
import math
|
|
6
6
|
import os
|
|
7
7
|
import time
|
|
8
|
-
import numpy as np
|
|
9
|
-
import talib
|
|
10
8
|
from dataclasses import dataclass
|
|
11
9
|
from typing import Optional, List, Dict, Any, Tuple
|
|
12
10
|
|
|
@@ -18,11 +16,18 @@ from utils.constants import AUTO_TRADES_LOG_FILE, INTERVAL_TO_MS, cp
|
|
|
18
16
|
from utils.helpers import parse_interval_list, init_clients, _try_float, parse_fractional_pct, \
|
|
19
17
|
extract_account_balance_from_user_state, round_size_for_hyperliquid, get_user_state_with_retry, get_all_mids, \
|
|
20
18
|
fetch_recent_candles, compute_position_unrealized_pnl, close_clients, compute_default_stop_loss_pct, \
|
|
21
|
-
extract_closed_pnl_from_fill, is_rate_limit_error
|
|
19
|
+
extract_closed_pnl_from_fill, is_rate_limit_error, get_user_fills_since
|
|
22
20
|
|
|
23
21
|
logger = logging.getLogger(__name__)
|
|
24
22
|
_AUTO_TRADES_LOGGER: Optional[logging.Logger] = None
|
|
25
23
|
|
|
24
|
+
try:
|
|
25
|
+
import numpy as np
|
|
26
|
+
import talib
|
|
27
|
+
except Exception: # TA-Lib is only required for the auto command.
|
|
28
|
+
np = None # type: ignore[assignment]
|
|
29
|
+
talib = None # type: ignore[assignment]
|
|
30
|
+
|
|
26
31
|
|
|
27
32
|
def get_auto_trades_logger() -> logging.Logger:
|
|
28
33
|
"""Return the dedicated auto-trade completion logger."""
|
|
@@ -825,7 +830,7 @@ async def build_auto_scan_loop_snapshot(
|
|
|
825
830
|
account_balance = extract_account_balance_from_user_state(user_state)
|
|
826
831
|
|
|
827
832
|
mids_task = asyncio.create_task(get_all_mids(info))
|
|
828
|
-
fills_task = asyncio.create_task(info
|
|
833
|
+
fills_task = asyncio.create_task(get_user_fills_since(info, account_address, metrics_start_time_ms))
|
|
829
834
|
|
|
830
835
|
mids = await mids_task
|
|
831
836
|
|
|
@@ -2410,7 +2415,12 @@ async def run_auto_trader(
|
|
|
2410
2415
|
launch_specs.append((candidate.coin, candidate.decision, resolved_size))
|
|
2411
2416
|
reserved_coins.add(candidate.coin)
|
|
2412
2417
|
if len(launch_specs) >= available_slots:
|
|
2413
|
-
|
|
2418
|
+
print(
|
|
2419
|
+
f"[AUTO] Reached max new launches for this scan batch "
|
|
2420
|
+
f"({len(launch_specs)}/{available_slots} available slot(s)); "
|
|
2421
|
+
f"deferring remaining candidates."
|
|
2422
|
+
)
|
|
2423
|
+
break
|
|
2414
2424
|
except Exception as exc:
|
|
2415
2425
|
logger.error(exc)
|
|
2416
2426
|
|
|
@@ -13,7 +13,7 @@ from utils.helpers import init_clients, get_position_for_coin, \
|
|
|
13
13
|
position_is_directional_add, \
|
|
14
14
|
fmt_optional_float, format_account_metrics, is_rate_limit_error, \
|
|
15
15
|
round_size_for_hyperliquid, get_position_size_for_coin, AccountRuntimeMetrics, get_best_bid_ask, \
|
|
16
|
-
round_price_for_hyperliquid, extract_order_error, close_clients, fetch_recent_candles
|
|
16
|
+
round_price_for_hyperliquid, extract_order_error, close_clients, fetch_recent_candles, get_ws_cache
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
19
|
import numpy as np
|
|
@@ -57,10 +57,17 @@ async def get_frontend_open_orders_for_coin(
|
|
|
57
57
|
info: Info,
|
|
58
58
|
account_address: str,
|
|
59
59
|
coin: str,
|
|
60
|
+
*,
|
|
61
|
+
force_http: bool = False,
|
|
60
62
|
) -> List[Dict[str, Any]]:
|
|
61
63
|
"""Return frontend open orders for a coin, including reduceOnly/isTrigger fields."""
|
|
62
64
|
try:
|
|
63
|
-
|
|
65
|
+
cache = get_ws_cache(info)
|
|
66
|
+
if cache is not None and not force_http:
|
|
67
|
+
cached_orders = await cache.get_frontend_open_orders(force_refresh=False)
|
|
68
|
+
orders = cached_orders if cached_orders is not None else await info.frontend_open_orders(account_address)
|
|
69
|
+
else:
|
|
70
|
+
orders = await info.frontend_open_orders(account_address)
|
|
64
71
|
except Exception:
|
|
65
72
|
logging.getLogger("hypertrader").exception(
|
|
66
73
|
"[WARN] Failed to fetch frontend open orders for %s.",
|
|
@@ -308,7 +315,7 @@ async def cancel_entry_orders_for_coin(
|
|
|
308
315
|
order snapshot and removes any leftover non-reduce-only orders for the same
|
|
309
316
|
coin/side. Reduce-only TP orders are intentionally left alone.
|
|
310
317
|
"""
|
|
311
|
-
orders = await get_frontend_open_orders_for_coin(info, account_address, coin)
|
|
318
|
+
orders = await get_frontend_open_orders_for_coin(info, account_address, coin, force_http=True)
|
|
312
319
|
oids: List[int] = []
|
|
313
320
|
for order in orders:
|
|
314
321
|
try:
|
|
@@ -327,7 +334,7 @@ async def cancel_entry_orders_for_coin(
|
|
|
327
334
|
# limit orders, so anything found here before market fallback is treated as
|
|
328
335
|
# an entry-order candidate.
|
|
329
336
|
if not oids:
|
|
330
|
-
fallback_orders = await get_open_orders_for_coin(info, account_address, coin)
|
|
337
|
+
fallback_orders = await get_open_orders_for_coin(info, account_address, coin, force_http=True)
|
|
331
338
|
for order in fallback_orders:
|
|
332
339
|
try:
|
|
333
340
|
if is_buy is not None and not order_side_matches(order, is_buy):
|
|
@@ -395,7 +402,7 @@ async def cancel_reduce_only_orders_for_coin(
|
|
|
395
402
|
only_tpsl: bool = False,
|
|
396
403
|
) -> None:
|
|
397
404
|
"""Cancel reduce-only orders for a coin, optionally limiting to TP/SL trigger orders."""
|
|
398
|
-
orders = await get_frontend_open_orders_for_coin(info, account_address, coin)
|
|
405
|
+
orders = await get_frontend_open_orders_for_coin(info, account_address, coin, force_http=True)
|
|
399
406
|
oids: List[int] = []
|
|
400
407
|
for order in orders:
|
|
401
408
|
try:
|
|
@@ -418,7 +425,7 @@ async def get_open_reduce_only_take_profit_orders_for_coin(
|
|
|
418
425
|
coin: str,
|
|
419
426
|
) -> List[Dict[str, Any]]:
|
|
420
427
|
"""Return open reduce-only non-trigger TP limit orders for a coin."""
|
|
421
|
-
orders = await get_frontend_open_orders_for_coin(info, account_address, coin)
|
|
428
|
+
orders = await get_frontend_open_orders_for_coin(info, account_address, coin, force_http=False)
|
|
422
429
|
tp_orders: List[Dict[str, Any]] = []
|
|
423
430
|
for order in orders:
|
|
424
431
|
try:
|
|
@@ -532,7 +539,7 @@ async def exit_on_tp_reversal(
|
|
|
532
539
|
limit_oids = extract_resting_oids(limit_resp)
|
|
533
540
|
|
|
534
541
|
await asyncio.sleep(min(max(poll_interval * 0.25, 0.1), 0.5))
|
|
535
|
-
live_pos = await get_position_for_coin(info, account_address, coin)
|
|
542
|
+
live_pos = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
536
543
|
if live_pos is None:
|
|
537
544
|
print(f"[TP-REVERSAL EXIT] {coin} fully closed on the limit order.")
|
|
538
545
|
return True
|
|
@@ -568,7 +575,7 @@ async def exit_on_tp_reversal(
|
|
|
568
575
|
deadline = time.time() + wait_seconds
|
|
569
576
|
while time.time() < deadline:
|
|
570
577
|
await asyncio.sleep(min(max(poll_interval, 0.2), 1.0))
|
|
571
|
-
live_pos = await get_position_for_coin(info, account_address, coin)
|
|
578
|
+
live_pos = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
572
579
|
if live_pos is None:
|
|
573
580
|
print(f"[TP-REVERSAL EXIT] {coin} flat after paired limit/stop exit.")
|
|
574
581
|
return True
|
|
@@ -587,7 +594,7 @@ async def exit_on_tp_reversal(
|
|
|
587
594
|
if stop_oid is not None:
|
|
588
595
|
await cancel_oids(exchange, coin, [stop_oid], "tp reversal stop orders")
|
|
589
596
|
|
|
590
|
-
live_pos = await get_position_for_coin(info, account_address, coin)
|
|
597
|
+
live_pos = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
591
598
|
if live_pos is None:
|
|
592
599
|
print(f"[TP-REVERSAL EXIT] {coin} flat after canceling remaining exit orders.")
|
|
593
600
|
return True
|
|
@@ -621,7 +628,7 @@ async def close_position_remainder_with_market_retries(
|
|
|
621
628
|
await cancel_reduce_only_orders_for_coin(info, exchange, account_address, coin, only_tpsl=False)
|
|
622
629
|
await asyncio.sleep(settle_before_close)
|
|
623
630
|
|
|
624
|
-
remainder_pos = await get_position_for_coin(info, account_address, coin)
|
|
631
|
+
remainder_pos = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
625
632
|
if remainder_pos is None:
|
|
626
633
|
print(f"[{label}] {coin} is already flat after cleanup.")
|
|
627
634
|
return True
|
|
@@ -651,7 +658,7 @@ async def close_position_remainder_with_market_retries(
|
|
|
651
658
|
continue
|
|
652
659
|
|
|
653
660
|
await asyncio.sleep(settle_after_close)
|
|
654
|
-
final_remainder_pos = await get_position_for_coin(info, account_address, coin)
|
|
661
|
+
final_remainder_pos = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
655
662
|
if final_remainder_pos is None:
|
|
656
663
|
print(f"[{label}] {coin} fully closed after residual market exit.")
|
|
657
664
|
return True
|
|
@@ -763,7 +770,7 @@ async def enter_position_with_reposting_limit(
|
|
|
763
770
|
raise RuntimeError("entry_tif must be Alo or Gtc.")
|
|
764
771
|
|
|
765
772
|
size = await round_size_for_hyperliquid(info, coin, size)
|
|
766
|
-
initial_pos = await get_position_size_for_coin(info, account_address, coin)
|
|
773
|
+
initial_pos = await get_position_size_for_coin(info, account_address, coin, force_http=True)
|
|
767
774
|
direction_label = "LONG" if is_buy else "SHORT"
|
|
768
775
|
|
|
769
776
|
if initial_pos != 0.0:
|
|
@@ -789,11 +796,11 @@ async def enter_position_with_reposting_limit(
|
|
|
789
796
|
|
|
790
797
|
async def get_entry_progress() -> Tuple[float, float, float, str, AccountRuntimeMetrics]:
|
|
791
798
|
"""Return current_pos, filled_delta, remaining_size, formatted uPnL, account metrics."""
|
|
792
|
-
_current_pos = await get_position_size_for_coin(info, account_address, coin)
|
|
799
|
+
_current_pos = await get_position_size_for_coin(info, account_address, coin, force_http=True)
|
|
793
800
|
filled_delta = signed_position_delta(initial_pos, _current_pos, is_buy)
|
|
794
801
|
remaining_size = max(0.0, size - filled_delta)
|
|
795
802
|
|
|
796
|
-
pos_snapshot = await get_position_for_coin(info, account_address, coin)
|
|
803
|
+
pos_snapshot = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
797
804
|
mids = await get_all_mids(info)
|
|
798
805
|
mid_price_raw = mids.get(coin)
|
|
799
806
|
_upnl_str = "N/A"
|
|
@@ -872,6 +879,7 @@ async def enter_position_with_reposting_limit(
|
|
|
872
879
|
f"[ENTRY {attempt}/{retries}] modifying oid={active_oid} -> "
|
|
873
880
|
f"{side} {remaining:.8f} {coin} @ {limit_px:.8f} ({entry_tif}, top-of-book)"
|
|
874
881
|
)
|
|
882
|
+
prev_active_oid = active_oid
|
|
875
883
|
try:
|
|
876
884
|
resp = await exchange.modify_order(
|
|
877
885
|
active_oid,
|
|
@@ -899,6 +907,24 @@ async def enter_position_with_reposting_limit(
|
|
|
899
907
|
await asyncio.sleep(repost_interval)
|
|
900
908
|
continue
|
|
901
909
|
|
|
910
|
+
resting_oids = extract_resting_oids(resp)
|
|
911
|
+
if resting_oids:
|
|
912
|
+
new_oid = int(resting_oids[0])
|
|
913
|
+
if new_oid != active_oid:
|
|
914
|
+
print(f"[ENTRY {attempt}] active entry oid updated {active_oid} -> {new_oid}")
|
|
915
|
+
active_oid = new_oid
|
|
916
|
+
else:
|
|
917
|
+
filled_now = extract_filled_qty(resp)
|
|
918
|
+
if filled_now > 0.0:
|
|
919
|
+
print(f"[ENTRY {attempt}] modify filled {filled_now:.8f}; clearing active oid.")
|
|
920
|
+
active_oid = None
|
|
921
|
+
else:
|
|
922
|
+
print(
|
|
923
|
+
f"[ENTRY {attempt}] modify returned no resting oid or fill for prior oid={prev_active_oid}; "
|
|
924
|
+
"will re-check position before another modify."
|
|
925
|
+
)
|
|
926
|
+
active_oid = None
|
|
927
|
+
|
|
902
928
|
await asyncio.sleep(repost_interval)
|
|
903
929
|
|
|
904
930
|
current_pos, filled, remaining, upnl_str, metrics = await get_entry_progress()
|
|
@@ -965,7 +991,7 @@ async def enter_position_with_reposting_limit(
|
|
|
965
991
|
)
|
|
966
992
|
|
|
967
993
|
await asyncio.sleep(max(0.25, repost_interval))
|
|
968
|
-
final_pos = await get_position_for_coin(info, account_address, coin)
|
|
994
|
+
final_pos = await get_position_for_coin(info, account_address, coin, force_http=True)
|
|
969
995
|
if final_pos is None:
|
|
970
996
|
raise RuntimeError(f"No open {coin} position found after entry attempts.")
|
|
971
997
|
|
|
@@ -1213,6 +1239,8 @@ async def monitor_bracket_position(
|
|
|
1213
1239
|
favorable_extreme: Optional[float] = None
|
|
1214
1240
|
tp_zone_seen = False
|
|
1215
1241
|
tp_remainder_close_in_progress = False
|
|
1242
|
+
tp_remainder_baseline_abs = abs(managed_signed_size)
|
|
1243
|
+
tp_remainder_fill_confirmed = False
|
|
1216
1244
|
|
|
1217
1245
|
try:
|
|
1218
1246
|
while True:
|
|
@@ -1284,6 +1312,8 @@ async def monitor_bracket_position(
|
|
|
1284
1312
|
hidden_tp_oids.clear()
|
|
1285
1313
|
tp_level_to_oid = build_tp_level_to_oid_map(tp_orders, tp_oids)
|
|
1286
1314
|
tp_remainder_close_in_progress = False
|
|
1315
|
+
tp_remainder_baseline_abs = live_pos_abs
|
|
1316
|
+
tp_remainder_fill_confirmed = False
|
|
1287
1317
|
if hide_orders:
|
|
1288
1318
|
local_stop_px, stop_source = resolve_stop_loss_trigger_px(
|
|
1289
1319
|
side,
|
|
@@ -1295,6 +1325,9 @@ async def monitor_bracket_position(
|
|
|
1295
1325
|
managed_signed_size = live_signed_size
|
|
1296
1326
|
entry_px = live_entry_px
|
|
1297
1327
|
|
|
1328
|
+
if live_pos_abs + PRICE_EPS < tp_remainder_baseline_abs:
|
|
1329
|
+
tp_remainder_fill_confirmed = True
|
|
1330
|
+
|
|
1298
1331
|
if dynamic_auto_sar_stop_enabled:
|
|
1299
1332
|
try:
|
|
1300
1333
|
updated_sar_stop_px, latest_sar_px, latest_sar_close_px, sar_flip_exit = await compute_auto_sar_stop_state(
|
|
@@ -1495,6 +1528,13 @@ async def monitor_bracket_position(
|
|
|
1495
1528
|
tp_levels_exhausted = len(open_tp_orders) == 0 and not trailing_tp_armed
|
|
1496
1529
|
|
|
1497
1530
|
if tp_levels_exhausted and not tp_remainder_close_in_progress:
|
|
1531
|
+
if not tp_remainder_fill_confirmed:
|
|
1532
|
+
print(
|
|
1533
|
+
f"[TP-REMAINDER-WARN] {coin} TP ladder is absent but no TP fill was confirmed "
|
|
1534
|
+
f"(live_abs={live_pos_abs:.8f}, baseline_abs={tp_remainder_baseline_abs:.8f}). "
|
|
1535
|
+
"Leaving the position open."
|
|
1536
|
+
)
|
|
1537
|
+
continue
|
|
1498
1538
|
tp_remainder_close_in_progress = True
|
|
1499
1539
|
print(
|
|
1500
1540
|
f"[TP-REMAINDER] {coin} take-profit ladder is exhausted; "
|
|
@@ -6,11 +6,11 @@ from typing import Optional, List, Dict, Any
|
|
|
6
6
|
from hyperliquid.exchange import Exchange
|
|
7
7
|
from hyperliquid.info import Info
|
|
8
8
|
|
|
9
|
-
from modes.auto_trader import compute_default_stop_loss_pct
|
|
10
9
|
from modes.position_management import monitor_bracket_position, rebuild_bracket_orders
|
|
11
10
|
from utils.constants import WATCH_RETRY_SLEEP_SECONDS
|
|
12
11
|
from utils.helpers import init_clients, get_all_open_positions, get_all_mids, compute_position_unrealized_pnl, \
|
|
13
|
-
get_account_runtime_metrics, format_account_metrics, is_rate_limit_error, close_clients
|
|
12
|
+
get_account_runtime_metrics, format_account_metrics, is_rate_limit_error, close_clients, \
|
|
13
|
+
compute_default_stop_loss_pct
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
async def attach_bracket_to_existing_position(
|
|
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
|
|
|
9
9
|
|
|
10
10
|
setup(
|
|
11
11
|
name="hypertrader",
|
|
12
|
-
version="0.1.
|
|
12
|
+
version="0.1.6",
|
|
13
13
|
description="Async Hyperliquid trading helper built on the async SDK.",
|
|
14
14
|
long_description=README.read_text(encoding="utf-8"),
|
|
15
15
|
long_description_content_type="text/markdown",
|
|
@@ -7,8 +7,6 @@ from dataclasses import dataclass
|
|
|
7
7
|
from typing import List, Any, Tuple, Optional, Dict, Awaitable
|
|
8
8
|
|
|
9
9
|
import eth_account
|
|
10
|
-
import numpy as np
|
|
11
|
-
import talib
|
|
12
10
|
from dotenv import load_dotenv
|
|
13
11
|
from eth_account.signers.local import LocalAccount
|
|
14
12
|
from hyperliquid.exchange import Exchange
|
|
@@ -69,11 +67,13 @@ def parse_fractional_pct(value: Any, *, field_name: str) -> float:
|
|
|
69
67
|
# Credentials / clients
|
|
70
68
|
# ---------------------------------------------------------------------------
|
|
71
69
|
|
|
72
|
-
def load_credentials() -> Tuple[str, str]:
|
|
70
|
+
def load_credentials(testnet: bool =False) -> Tuple[str, str]:
|
|
73
71
|
"""Load Hyperliquid credentials from environment variables."""
|
|
74
72
|
load_dotenv()
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
if testnet:
|
|
74
|
+
secret_key = os.getenv("HYPERLIQUID_TESTNET_SECRET_KEY")
|
|
75
|
+
else:
|
|
76
|
+
secret_key = os.getenv("HYPERLIQUID_SECRET_KEY")
|
|
77
77
|
account_address = os.getenv("HYPERLIQUID_ACCOUNT_ADDRESS")
|
|
78
78
|
|
|
79
79
|
if not secret_key:
|
|
@@ -102,8 +102,9 @@ class HyperliquidWebsocketCache:
|
|
|
102
102
|
* userEvents -> cached for diagnostics / future event-driven position refresh
|
|
103
103
|
* orderUpdates -> cached for diagnostics / future order-state refresh
|
|
104
104
|
|
|
105
|
-
|
|
106
|
-
|
|
105
|
+
There is no native websocket subscription for clearinghouseState or openOrders in
|
|
106
|
+
the async SDK. For those, this class keeps REST-backed snapshots that are marked
|
|
107
|
+
dirty by userEvents / userFills / orderUpdates and refreshed on demand.
|
|
107
108
|
"""
|
|
108
109
|
|
|
109
110
|
def __init__(
|
|
@@ -131,9 +132,36 @@ class HyperliquidWebsocketCache:
|
|
|
131
132
|
self._bbo_sub_ids: Dict[str, int] = {}
|
|
132
133
|
|
|
133
134
|
self._user_events_sub_id: Optional[int] = None
|
|
135
|
+
self._user_fills_sub_id: Optional[int] = None
|
|
134
136
|
self._order_updates_sub_id: Optional[int] = None
|
|
135
137
|
self.last_user_event: Optional[Dict[str, Any]] = None
|
|
138
|
+
self.last_user_fills: Optional[Dict[str, Any]] = None
|
|
136
139
|
self.last_order_update: Optional[Dict[str, Any]] = None
|
|
140
|
+
|
|
141
|
+
self._user_state: Optional[Dict[str, Any]] = None
|
|
142
|
+
self._user_state_updated_at = 0.0
|
|
143
|
+
self._user_state_ready = asyncio.Event()
|
|
144
|
+
self._user_state_lock = asyncio.Lock()
|
|
145
|
+
self._user_state_dirty = True
|
|
146
|
+
|
|
147
|
+
self._open_orders: List[Dict[str, Any]] = []
|
|
148
|
+
self._open_orders_updated_at = 0.0
|
|
149
|
+
self._open_orders_ready = asyncio.Event()
|
|
150
|
+
self._open_orders_lock = asyncio.Lock()
|
|
151
|
+
self._open_orders_dirty = True
|
|
152
|
+
|
|
153
|
+
self._frontend_open_orders: List[Dict[str, Any]] = []
|
|
154
|
+
self._frontend_open_orders_updated_at = 0.0
|
|
155
|
+
self._frontend_open_orders_ready = asyncio.Event()
|
|
156
|
+
self._frontend_open_orders_lock = asyncio.Lock()
|
|
157
|
+
self._frontend_open_orders_dirty = True
|
|
158
|
+
|
|
159
|
+
self._user_fills: List[Dict[str, Any]] = []
|
|
160
|
+
self._user_fill_keys: set[Tuple[Any, ...]] = set()
|
|
161
|
+
self._user_fills_updated_at = 0.0
|
|
162
|
+
self._user_fills_ready = asyncio.Event()
|
|
163
|
+
self._user_fills_lock = asyncio.Lock()
|
|
164
|
+
self._user_fills_seed_start_ms: Optional[int] = None
|
|
137
165
|
self._candle_buffers: Dict[Tuple[str, str], List[Dict[str, Any]]] = {}
|
|
138
166
|
self._candle_requested_periods: Dict[Tuple[str, str], int] = {}
|
|
139
167
|
self._candle_updated_at: Dict[Tuple[str, str], float] = {}
|
|
@@ -224,6 +252,8 @@ class HyperliquidWebsocketCache:
|
|
|
224
252
|
for sub, attr_name, label, callback in (
|
|
225
253
|
({"type": "userEvents", "user": self.account_address}, "_user_events_sub_id", "userEvents",
|
|
226
254
|
self._on_user_event),
|
|
255
|
+
({"type": "userFills", "user": self.account_address}, "_user_fills_sub_id", "userFills",
|
|
256
|
+
self._on_user_fills),
|
|
227
257
|
({"type": "orderUpdates", "user": self.account_address}, "_order_updates_sub_id", "orderUpdates",
|
|
228
258
|
self._on_order_update),
|
|
229
259
|
):
|
|
@@ -242,6 +272,7 @@ class HyperliquidWebsocketCache:
|
|
|
242
272
|
unsubscribe_specs: List[Tuple[Dict[str, Any], Optional[int], str]] = [
|
|
243
273
|
({"type": "allMids"}, self._mids_sub_id, "allMids"),
|
|
244
274
|
({"type": "userEvents", "user": self.account_address}, self._user_events_sub_id, "userEvents"),
|
|
275
|
+
({"type": "userFills", "user": self.account_address}, self._user_fills_sub_id, "userFills"),
|
|
245
276
|
({"type": "orderUpdates", "user": self.account_address}, self._order_updates_sub_id, "orderUpdates"),
|
|
246
277
|
]
|
|
247
278
|
for coin, sub_id in list(self._bbo_sub_ids.items()):
|
|
@@ -306,9 +337,30 @@ class HyperliquidWebsocketCache:
|
|
|
306
337
|
|
|
307
338
|
def _on_user_event(self, msg: Dict[str, Any]) -> None:
|
|
308
339
|
self.last_user_event = msg
|
|
340
|
+
data = msg.get("data", {}) if isinstance(msg, dict) else {}
|
|
341
|
+
if isinstance(data, dict):
|
|
342
|
+
fills = data.get("fills")
|
|
343
|
+
if isinstance(fills, list) and fills:
|
|
344
|
+
self._merge_user_fills(fills, replace=False)
|
|
345
|
+
self._user_state_dirty = True
|
|
346
|
+
self._open_orders_dirty = True
|
|
347
|
+
self._frontend_open_orders_dirty = True
|
|
348
|
+
|
|
349
|
+
def _on_user_fills(self, msg: Dict[str, Any]) -> None:
|
|
350
|
+
self.last_user_fills = msg
|
|
351
|
+
data = msg.get("data", {}) if isinstance(msg, dict) else {}
|
|
352
|
+
if not isinstance(data, dict):
|
|
353
|
+
return
|
|
354
|
+
fills = data.get("fills")
|
|
355
|
+
if isinstance(fills, list):
|
|
356
|
+
self._merge_user_fills(fills, replace=bool(data.get("isSnapshot")))
|
|
357
|
+
self._user_state_dirty = True
|
|
309
358
|
|
|
310
359
|
def _on_order_update(self, msg: Dict[str, Any]) -> None:
|
|
311
360
|
self.last_order_update = msg
|
|
361
|
+
self._open_orders_dirty = True
|
|
362
|
+
self._frontend_open_orders_dirty = True
|
|
363
|
+
self._user_state_dirty = True
|
|
312
364
|
|
|
313
365
|
def _on_candle(self, requested_coin: str, requested_interval: str, msg: Dict[str, Any]) -> None:
|
|
314
366
|
data = msg.get("data", {}) if isinstance(msg, dict) else {}
|
|
@@ -423,6 +475,157 @@ class HyperliquidWebsocketCache:
|
|
|
423
475
|
return None
|
|
424
476
|
return [dict(candle) for candle in candles[-periods:]]
|
|
425
477
|
|
|
478
|
+
@staticmethod
|
|
479
|
+
def _extract_fill_time_ms(fill: Dict[str, Any]) -> int:
|
|
480
|
+
for key in ("time", "timestamp"):
|
|
481
|
+
try:
|
|
482
|
+
value = fill.get(key)
|
|
483
|
+
if value is not None:
|
|
484
|
+
return int(float(value))
|
|
485
|
+
except (AttributeError, TypeError, ValueError):
|
|
486
|
+
continue
|
|
487
|
+
return 0
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def _fill_cache_key(cls, fill: Dict[str, Any]) -> Tuple[Any, ...]:
|
|
491
|
+
return (
|
|
492
|
+
fill.get("tid"),
|
|
493
|
+
fill.get("hash"),
|
|
494
|
+
fill.get("oid"),
|
|
495
|
+
fill.get("coin"),
|
|
496
|
+
fill.get("px"),
|
|
497
|
+
fill.get("sz"),
|
|
498
|
+
cls._extract_fill_time_ms(fill),
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
def _merge_user_fills(self, fills: List[Dict[str, Any]], replace: bool) -> None:
|
|
502
|
+
if replace:
|
|
503
|
+
self._user_fills = []
|
|
504
|
+
self._user_fill_keys.clear()
|
|
505
|
+
|
|
506
|
+
changed = False
|
|
507
|
+
for fill in fills:
|
|
508
|
+
if not isinstance(fill, dict):
|
|
509
|
+
continue
|
|
510
|
+
fill_copy = dict(fill)
|
|
511
|
+
cache_key = self._fill_cache_key(fill_copy)
|
|
512
|
+
if cache_key in self._user_fill_keys:
|
|
513
|
+
continue
|
|
514
|
+
self._user_fill_keys.add(cache_key)
|
|
515
|
+
self._user_fills.append(fill_copy)
|
|
516
|
+
changed = True
|
|
517
|
+
|
|
518
|
+
if changed or replace:
|
|
519
|
+
self._user_fills.sort(key=self._extract_fill_time_ms)
|
|
520
|
+
self._user_fills_updated_at = time.monotonic()
|
|
521
|
+
self._user_fills_ready.set()
|
|
522
|
+
|
|
523
|
+
async def get_user_state(self, *, force_refresh: bool = False) -> Optional[Dict[str, Any]]:
|
|
524
|
+
if not self.enabled:
|
|
525
|
+
return None
|
|
526
|
+
if (
|
|
527
|
+
not force_refresh
|
|
528
|
+
and not self._user_state_dirty
|
|
529
|
+
and self._is_fresh(self._user_state_updated_at, self.max_age_seconds)
|
|
530
|
+
and self._user_state is not None
|
|
531
|
+
):
|
|
532
|
+
return dict(self._user_state)
|
|
533
|
+
|
|
534
|
+
async with self._user_state_lock:
|
|
535
|
+
if (
|
|
536
|
+
not force_refresh
|
|
537
|
+
and not self._user_state_dirty
|
|
538
|
+
and self._is_fresh(self._user_state_updated_at, self.max_age_seconds)
|
|
539
|
+
and self._user_state is not None
|
|
540
|
+
):
|
|
541
|
+
return dict(self._user_state)
|
|
542
|
+
|
|
543
|
+
user_state = await self.info.user_state(self.account_address)
|
|
544
|
+
if not isinstance(user_state, dict):
|
|
545
|
+
return None
|
|
546
|
+
self._user_state = dict(user_state)
|
|
547
|
+
self._user_state_updated_at = time.monotonic()
|
|
548
|
+
self._user_state_dirty = False
|
|
549
|
+
self._user_state_ready.set()
|
|
550
|
+
return dict(self._user_state)
|
|
551
|
+
|
|
552
|
+
async def get_open_orders(self, *, force_refresh: bool = False) -> Optional[List[Dict[str, Any]]]:
|
|
553
|
+
if not self.enabled:
|
|
554
|
+
return None
|
|
555
|
+
if (
|
|
556
|
+
not force_refresh
|
|
557
|
+
and not self._open_orders_dirty
|
|
558
|
+
and self._is_fresh(self._open_orders_updated_at, self.max_age_seconds)
|
|
559
|
+
):
|
|
560
|
+
return [dict(order) for order in self._open_orders]
|
|
561
|
+
|
|
562
|
+
async with self._open_orders_lock:
|
|
563
|
+
if (
|
|
564
|
+
not force_refresh
|
|
565
|
+
and not self._open_orders_dirty
|
|
566
|
+
and self._is_fresh(self._open_orders_updated_at, self.max_age_seconds)
|
|
567
|
+
):
|
|
568
|
+
return [dict(order) for order in self._open_orders]
|
|
569
|
+
|
|
570
|
+
open_orders = await self.info.open_orders(self.account_address)
|
|
571
|
+
if not isinstance(open_orders, list):
|
|
572
|
+
return None
|
|
573
|
+
self._open_orders = [dict(order) for order in open_orders if isinstance(order, dict)]
|
|
574
|
+
self._open_orders_updated_at = time.monotonic()
|
|
575
|
+
self._open_orders_dirty = False
|
|
576
|
+
self._open_orders_ready.set()
|
|
577
|
+
return [dict(order) for order in self._open_orders]
|
|
578
|
+
|
|
579
|
+
async def get_frontend_open_orders(self, *, force_refresh: bool = False) -> Optional[List[Dict[str, Any]]]:
|
|
580
|
+
if not self.enabled:
|
|
581
|
+
return None
|
|
582
|
+
if (
|
|
583
|
+
not force_refresh
|
|
584
|
+
and not self._frontend_open_orders_dirty
|
|
585
|
+
and self._is_fresh(self._frontend_open_orders_updated_at, self.max_age_seconds)
|
|
586
|
+
):
|
|
587
|
+
return [dict(order) for order in self._frontend_open_orders]
|
|
588
|
+
|
|
589
|
+
async with self._frontend_open_orders_lock:
|
|
590
|
+
if (
|
|
591
|
+
not force_refresh
|
|
592
|
+
and not self._frontend_open_orders_dirty
|
|
593
|
+
and self._is_fresh(self._frontend_open_orders_updated_at, self.max_age_seconds)
|
|
594
|
+
):
|
|
595
|
+
return [dict(order) for order in self._frontend_open_orders]
|
|
596
|
+
|
|
597
|
+
frontend_orders = await self.info.frontend_open_orders(self.account_address)
|
|
598
|
+
if not isinstance(frontend_orders, list):
|
|
599
|
+
return None
|
|
600
|
+
self._frontend_open_orders = [dict(order) for order in frontend_orders if isinstance(order, dict)]
|
|
601
|
+
self._frontend_open_orders_updated_at = time.monotonic()
|
|
602
|
+
self._frontend_open_orders_dirty = False
|
|
603
|
+
self._frontend_open_orders_ready.set()
|
|
604
|
+
return [dict(order) for order in self._frontend_open_orders]
|
|
605
|
+
|
|
606
|
+
async def get_user_fills_since(self, start_time_ms: int) -> Optional[List[Dict[str, Any]]]:
|
|
607
|
+
if not self.enabled:
|
|
608
|
+
return None
|
|
609
|
+
|
|
610
|
+
async with self._user_fills_lock:
|
|
611
|
+
need_seed = (
|
|
612
|
+
self._user_fills_seed_start_ms is None
|
|
613
|
+
or start_time_ms < self._user_fills_seed_start_ms
|
|
614
|
+
or not self._user_fills_ready.is_set()
|
|
615
|
+
)
|
|
616
|
+
if need_seed:
|
|
617
|
+
fills = await self.info.user_fills_by_time(self.account_address, start_time_ms)
|
|
618
|
+
if not isinstance(fills, list):
|
|
619
|
+
return None
|
|
620
|
+
self._user_fills_seed_start_ms = start_time_ms
|
|
621
|
+
self._merge_user_fills([dict(fill) for fill in fills if isinstance(fill, dict)], replace=True)
|
|
622
|
+
|
|
623
|
+
return [
|
|
624
|
+
dict(fill)
|
|
625
|
+
for fill in self._user_fills
|
|
626
|
+
if self._extract_fill_time_ms(fill) >= start_time_ms
|
|
627
|
+
]
|
|
628
|
+
|
|
426
629
|
|
|
427
630
|
def get_ws_cache(info: Info) -> Optional[HyperliquidWebsocketCache]:
|
|
428
631
|
cache = getattr(info, "ws_cache", None)
|
|
@@ -433,7 +636,7 @@ def get_ws_cache(info: Info) -> Optional[HyperliquidWebsocketCache]:
|
|
|
433
636
|
|
|
434
637
|
async def init_clients(use_testnet: bool, use_websocket: bool = True) -> Tuple[str, Info, Exchange]:
|
|
435
638
|
"""Initialize async Info and Exchange clients for Hyperliquid."""
|
|
436
|
-
secret_key, account_address = load_credentials()
|
|
639
|
+
secret_key, account_address = load_credentials(use_testnet)
|
|
437
640
|
api_url = constants.TESTNET_API_URL if use_testnet else constants.MAINNET_API_URL
|
|
438
641
|
account: LocalAccount = eth_account.Account.from_key(secret_key)
|
|
439
642
|
|
|
@@ -585,10 +788,22 @@ async def get_best_bid_ask(info: Info, coin: str) -> Tuple[Optional[float], Opti
|
|
|
585
788
|
return best_bid, best_ask
|
|
586
789
|
|
|
587
790
|
|
|
588
|
-
async def get_open_orders_for_coin(
|
|
791
|
+
async def get_open_orders_for_coin(
|
|
792
|
+
info: Info,
|
|
793
|
+
account_address: str,
|
|
794
|
+
coin: str,
|
|
795
|
+
*,
|
|
796
|
+
force_http: bool = False,
|
|
797
|
+
) -> List[Dict[str, Any]]:
|
|
589
798
|
"""Return open orders for a specific coin."""
|
|
590
799
|
try:
|
|
591
|
-
open_orders
|
|
800
|
+
open_orders: Any
|
|
801
|
+
cache = get_ws_cache(info)
|
|
802
|
+
if cache is not None and not force_http:
|
|
803
|
+
cached_orders = await cache.get_open_orders(force_refresh=False)
|
|
804
|
+
open_orders = cached_orders if cached_orders is not None else await info.open_orders(account_address)
|
|
805
|
+
else:
|
|
806
|
+
open_orders = await info.open_orders(account_address)
|
|
592
807
|
except Exception:
|
|
593
808
|
logging.getLogger("hypertrader").exception(
|
|
594
809
|
"[WARN] Failed to fetch open orders for %s.",
|
|
@@ -627,6 +842,7 @@ async def get_user_state_with_retry(
|
|
|
627
842
|
context_label: str,
|
|
628
843
|
coin: Optional[str] = None,
|
|
629
844
|
retry_sleep: float = WATCH_RETRY_SLEEP_SECONDS,
|
|
845
|
+
force_http: bool = False,
|
|
630
846
|
) -> Dict[str, Any]:
|
|
631
847
|
"""Fetch user_state and keep retrying on Hyperliquid rate limits."""
|
|
632
848
|
coin_label = coin.upper() if coin else "ALL"
|
|
@@ -634,7 +850,12 @@ async def get_user_state_with_retry(
|
|
|
634
850
|
while True:
|
|
635
851
|
attempt += 1
|
|
636
852
|
try:
|
|
637
|
-
|
|
853
|
+
cache = get_ws_cache(info)
|
|
854
|
+
if cache is not None and not force_http:
|
|
855
|
+
cached_state = await cache.get_user_state(force_refresh=False)
|
|
856
|
+
user_state = cached_state if cached_state is not None else await info.user_state(account_address)
|
|
857
|
+
else:
|
|
858
|
+
user_state = await info.user_state(account_address)
|
|
638
859
|
except asyncio.CancelledError:
|
|
639
860
|
raise
|
|
640
861
|
except Exception as exc:
|
|
@@ -656,12 +877,18 @@ async def get_user_state_with_retry(
|
|
|
656
877
|
# Position helpers
|
|
657
878
|
# ---------------------------------------------------------------------------
|
|
658
879
|
|
|
659
|
-
async def get_all_open_positions(
|
|
880
|
+
async def get_all_open_positions(
|
|
881
|
+
info: Info,
|
|
882
|
+
account_address: str,
|
|
883
|
+
*,
|
|
884
|
+
force_http: bool = False,
|
|
885
|
+
) -> List[Dict[str, Any]]:
|
|
660
886
|
"""Return a list of open perp positions for the account."""
|
|
661
887
|
user_state = await get_user_state_with_retry(
|
|
662
888
|
info,
|
|
663
889
|
account_address,
|
|
664
890
|
context_label="get_all_open_positions",
|
|
891
|
+
force_http=force_http,
|
|
665
892
|
)
|
|
666
893
|
asset_positions = user_state.get("assetPositions", [])
|
|
667
894
|
open_positions: List[Dict[str, Any]] = []
|
|
@@ -678,13 +905,20 @@ async def get_all_open_positions(info: Info, account_address: str) -> List[Dict[
|
|
|
678
905
|
return open_positions
|
|
679
906
|
|
|
680
907
|
|
|
681
|
-
async def get_position_size_for_coin(
|
|
908
|
+
async def get_position_size_for_coin(
|
|
909
|
+
info: Info,
|
|
910
|
+
account_address: str,
|
|
911
|
+
coin: str,
|
|
912
|
+
*,
|
|
913
|
+
force_http: bool = False,
|
|
914
|
+
) -> float:
|
|
682
915
|
"""Return signed position size for a coin, or 0.0 if flat/not found."""
|
|
683
916
|
user_state = await get_user_state_with_retry(
|
|
684
917
|
info,
|
|
685
918
|
account_address,
|
|
686
919
|
context_label="get_position_size_for_coin",
|
|
687
920
|
coin=coin,
|
|
921
|
+
force_http=force_http,
|
|
688
922
|
)
|
|
689
923
|
asset_positions = user_state.get("assetPositions", [])
|
|
690
924
|
for asset_pos in asset_positions:
|
|
@@ -701,6 +935,8 @@ async def get_position_for_coin(
|
|
|
701
935
|
info: Info,
|
|
702
936
|
account_address: str,
|
|
703
937
|
coin: str,
|
|
938
|
+
*,
|
|
939
|
+
force_http: bool = False,
|
|
704
940
|
) -> Optional[Dict[str, Any]]:
|
|
705
941
|
"""Return full position dict for a coin if the account has a non-zero position."""
|
|
706
942
|
user_state = await get_user_state_with_retry(
|
|
@@ -708,6 +944,7 @@ async def get_position_for_coin(
|
|
|
708
944
|
account_address,
|
|
709
945
|
context_label="get_position_for_coin",
|
|
710
946
|
coin=coin,
|
|
947
|
+
force_http=force_http,
|
|
711
948
|
)
|
|
712
949
|
for asset_pos in user_state.get("assetPositions", []):
|
|
713
950
|
pos = asset_pos.get("position", {})
|
|
@@ -859,7 +1096,7 @@ async def get_realized_pnl_since(
|
|
|
859
1096
|
return None
|
|
860
1097
|
|
|
861
1098
|
try:
|
|
862
|
-
fills = await info
|
|
1099
|
+
fills = await get_user_fills_since(info, account_address, start_time_ms)
|
|
863
1100
|
except Exception:
|
|
864
1101
|
logging.getLogger("hypertrader").exception(
|
|
865
1102
|
"[METRICS] Failed to fetch user fills since %s for coin=%s.",
|
|
@@ -886,6 +1123,29 @@ async def get_realized_pnl_since(
|
|
|
886
1123
|
found = True
|
|
887
1124
|
return total if found or fills else 0.0
|
|
888
1125
|
|
|
1126
|
+
|
|
1127
|
+
async def get_user_fills_since(
|
|
1128
|
+
info: Info,
|
|
1129
|
+
account_address: str,
|
|
1130
|
+
start_time_ms: int,
|
|
1131
|
+
*,
|
|
1132
|
+
end_time_ms: Optional[int] = None,
|
|
1133
|
+
aggregate_by_time: bool = False,
|
|
1134
|
+
force_http: bool = False,
|
|
1135
|
+
) -> Any:
|
|
1136
|
+
"""Return fills since start_time_ms, using the websocket-backed cache when safe."""
|
|
1137
|
+
cache = get_ws_cache(info)
|
|
1138
|
+
if cache is not None and not force_http and end_time_ms is None and not aggregate_by_time:
|
|
1139
|
+
cached_fills = await cache.get_user_fills_since(start_time_ms)
|
|
1140
|
+
if cached_fills is not None:
|
|
1141
|
+
return cached_fills
|
|
1142
|
+
return await info.user_fills_by_time(
|
|
1143
|
+
account_address,
|
|
1144
|
+
start_time_ms,
|
|
1145
|
+
end_time_ms,
|
|
1146
|
+
aggregate_by_time=aggregate_by_time,
|
|
1147
|
+
)
|
|
1148
|
+
|
|
889
1149
|
def extract_account_balance_from_user_state(user_state: Dict[str, Any]) -> Optional[float]:
|
|
890
1150
|
"""Extract current Hyperliquid account value from a clearinghouseState response."""
|
|
891
1151
|
candidate_paths = (
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|