hypertrader 0.1.9__tar.gz → 0.2.0__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.
Files changed (45) hide show
  1. {hypertrader-0.1.9 → hypertrader-0.2.0}/PKG-INFO +2 -1
  2. {hypertrader-0.1.9 → hypertrader-0.2.0}/README.md +1 -0
  3. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.egg-info/PKG-INFO +2 -1
  4. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.egg-info/SOURCES.txt +3 -0
  5. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.py +18 -23
  6. {hypertrader-0.1.9 → hypertrader-0.2.0}/modes/auto_trader.py +12 -6
  7. {hypertrader-0.1.9 → hypertrader-0.2.0}/setup.py +1 -1
  8. hypertrader-0.2.0/tests/test_api_retry.py +127 -0
  9. hypertrader-0.2.0/tests/test_cli_shutdown.py +86 -0
  10. {hypertrader-0.1.9 → hypertrader-0.2.0}/utils/helpers.py +18 -6
  11. hypertrader-0.2.0/utils/retry.py +41 -0
  12. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.egg-info/dependency_links.txt +0 -0
  13. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.egg-info/entry_points.txt +0 -0
  14. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.egg-info/requires.txt +0 -0
  15. {hypertrader-0.1.9 → hypertrader-0.2.0}/hypertrader.egg-info/top_level.txt +0 -0
  16. {hypertrader-0.1.9 → hypertrader-0.2.0}/modes/market_maker.py +0 -0
  17. {hypertrader-0.1.9 → hypertrader-0.2.0}/modes/position_management.py +0 -0
  18. {hypertrader-0.1.9 → hypertrader-0.2.0}/modes/position_watcher.py +0 -0
  19. {hypertrader-0.1.9 → hypertrader-0.2.0}/modes/trailing_stop.py +0 -0
  20. {hypertrader-0.1.9 → hypertrader-0.2.0}/setup.cfg +0 -0
  21. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/__init__.py +0 -0
  22. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/base.py +0 -0
  23. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/default.py +0 -0
  24. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/fib_reversal.py +0 -0
  25. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/orderflow_metrics.py +0 -0
  26. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/orderflow_models.py +0 -0
  27. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/orderflow_pullback.py +0 -0
  28. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/registry.py +0 -0
  29. {hypertrader-0.1.9 → hypertrader-0.2.0}/strategies/reversal.py +0 -0
  30. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_async_hyperliquid.py +0 -0
  31. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_auto_dry_run.py +0 -0
  32. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_auto_strategy_registry.py +0 -0
  33. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_auto_trader_logging.py +0 -0
  34. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_fib_reversal_strategy.py +0 -0
  35. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_market_ids.py +0 -0
  36. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_orderflow_metrics.py +0 -0
  37. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_orderflow_pullback_lifecycle.py +0 -0
  38. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_reversal_strategy.py +0 -0
  39. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_simulated_logging.py +0 -0
  40. {hypertrader-0.1.9 → hypertrader-0.2.0}/tests/test_trailing_stop.py +0 -0
  41. {hypertrader-0.1.9 → hypertrader-0.2.0}/utils/constants.py +0 -0
  42. {hypertrader-0.1.9 → hypertrader-0.2.0}/utils/simulated_logging.py +0 -0
  43. {hypertrader-0.1.9 → hypertrader-0.2.0}/utils/style.py +0 -0
  44. {hypertrader-0.1.9 → hypertrader-0.2.0}/utils/timer.py +0 -0
  45. {hypertrader-0.1.9 → hypertrader-0.2.0}/utils/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -69,6 +69,7 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
69
69
  Install from pip: https://pypi.org/project/hypertrader
70
70
 
71
71
  <pre>
72
+ pip install --upgrade pip setuptools wheel
72
73
  pip install hypertrader[auto] --upgrade
73
74
  </pre>
74
75
 
@@ -38,6 +38,7 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
38
38
  Install from pip: https://pypi.org/project/hypertrader
39
39
 
40
40
  <pre>
41
+ pip install --upgrade pip setuptools wheel
41
42
  pip install hypertrader[auto] --upgrade
42
43
  </pre>
43
44
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -69,6 +69,7 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
69
69
  Install from pip: https://pypi.org/project/hypertrader
70
70
 
71
71
  <pre>
72
+ pip install --upgrade pip setuptools wheel
72
73
  pip install hypertrader[auto] --upgrade
73
74
  </pre>
74
75
 
@@ -21,10 +21,12 @@ strategies/orderflow_models.py
21
21
  strategies/orderflow_pullback.py
22
22
  strategies/registry.py
23
23
  strategies/reversal.py
24
+ tests/test_api_retry.py
24
25
  tests/test_async_hyperliquid.py
25
26
  tests/test_auto_dry_run.py
26
27
  tests/test_auto_strategy_registry.py
27
28
  tests/test_auto_trader_logging.py
29
+ tests/test_cli_shutdown.py
28
30
  tests/test_fib_reversal_strategy.py
29
31
  tests/test_market_ids.py
30
32
  tests/test_orderflow_metrics.py
@@ -34,6 +36,7 @@ tests/test_simulated_logging.py
34
36
  tests/test_trailing_stop.py
35
37
  utils/constants.py
36
38
  utils/helpers.py
39
+ utils/retry.py
37
40
  utils/simulated_logging.py
38
41
  utils/style.py
39
42
  utils/timer.py
@@ -1033,45 +1033,40 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
1033
1033
 
1034
1034
  def main(argv: Optional[List[str]] = None) -> None:
1035
1035
  from utils.simulated_logging import LoggedConsole
1036
- def start():
1037
- _loop = asyncio.new_event_loop()
1038
- asyncio.set_event_loop(_loop)
1039
- install_asyncio_exception_logging(_loop)
1036
+
1037
+ async def start() -> None:
1038
+ install_asyncio_exception_logging(asyncio.get_running_loop())
1040
1039
  print(f"[LOG] Writing runtime logs to {log_path}")
1041
1040
  print(f"[START] argv={argv if argv is not None else sys.argv[1:]}")
1042
- _loop.run_until_complete(async_main(argv))
1041
+ await async_main(argv)
1043
1042
 
1044
1043
  args = argv if argv is not None else sys.argv[1:]
1045
1044
  dry_run = "--dry-run" in args
1046
1045
  log_path = configure_runtime_logging(SIMULATED_LOG_FILE if dry_run else None)
1047
1046
  original_stdout, original_stderr = sys.stdout, sys.stderr
1048
- restart = False
1049
1047
  if dry_run:
1050
1048
  sys.stdout = LoggedConsole(original_stdout)
1051
1049
  # sys.stderr = LoggedConsole(original_stderr)
1052
1050
  try:
1053
- start()
1054
- except KeyboardInterrupt:
1055
- print("\n[!] Interrupted.")
1056
- except Exception as err:
1057
- logging.getLogger("hypertrader").exception("[FATAL] Unhandled main() exception: %s" % err)
1058
- restart = True
1059
-
1060
-
1051
+ # Give both the initial run and the single retry a complete lifecycle:
1052
+ # cancellation, awaited cleanup, then event-loop shutdown.
1053
+ for attempt in range(2):
1054
+ try:
1055
+ asyncio.run(start())
1056
+ break
1057
+ except KeyboardInterrupt:
1058
+ print("\n[!] Interrupted.")
1059
+ break
1060
+ except Exception as err:
1061
+ logging.getLogger("hypertrader").exception("[FATAL] Unhandled main() exception: %s", err)
1062
+ if attempt == 1:
1063
+ raise
1064
+ print('[!] Restarting ... ')
1061
1065
  finally:
1062
1066
  if dry_run:
1063
1067
  sys.stdout.flush()
1064
1068
  sys.stderr.flush()
1065
1069
  sys.stdout, sys.stderr = original_stdout, original_stderr
1066
- try:
1067
- loop = asyncio.get_event_loop()
1068
- except RuntimeError:
1069
- loop = None
1070
- if loop is not None and not loop.is_closed():
1071
- loop.close()
1072
- if restart:
1073
- print('[!] Restarting ... ')
1074
- start()
1075
1070
 
1076
1071
 
1077
1072
  if __name__ == "__main__":
@@ -18,6 +18,7 @@ from strategies.base import AutoStrategy, StrategyContext, StrategySignal
18
18
  from strategies.registry import create_strategy
19
19
  from strategies.reversal import normalize_signal_prices
20
20
  from utils.constants import AUTO_TRADES_LOG_FILE, SIMULATED_LOG_FILE, INTERVAL_TO_MS, PRICE_EPS, cp
21
+ from utils.retry import retry_api_read
21
22
  from utils.helpers import parse_interval_list, init_clients, _try_float, parse_fractional_pct, \
22
23
  extract_account_balance_from_user_state, round_size_for_hyperliquid, get_user_state_with_retry, get_all_mids, \
23
24
  fetch_recent_candles, compute_position_unrealized_pnl, close_clients, compute_default_stop_loss_pct, \
@@ -105,7 +106,9 @@ async def get_top_perp_markets_by_volume(info: Info, limit: int) -> List[Tuple[s
105
106
  if limit <= 0:
106
107
  raise RuntimeError("--top-markets must be > 0.")
107
108
 
108
- meta_and_ctxs = await info.meta_and_asset_ctxs()
109
+ meta_and_ctxs = await retry_api_read(
110
+ info.meta_and_asset_ctxs, context="auto market ranking"
111
+ )
109
112
  if not isinstance(meta_and_ctxs, (list, tuple)) or len(meta_and_ctxs) < 2:
110
113
  raise RuntimeError(f"Unexpected metaAndAssetCtxs response shape: {type(meta_and_ctxs).__name__}")
111
114
 
@@ -2883,11 +2886,14 @@ async def run_auto_trader(
2883
2886
  startup_backfill_state.begin_iteration()
2884
2887
 
2885
2888
  try:
2886
- shared_snapshot = await build_auto_scan_loop_snapshot(
2887
- info=info,
2888
- account_address=account_address,
2889
- metrics_start_time_ms=metrics_start_time_ms,
2890
- dry_run=dry_run,
2889
+ shared_snapshot = await retry_api_read(
2890
+ lambda: build_auto_scan_loop_snapshot(
2891
+ info=info,
2892
+ account_address=account_address,
2893
+ metrics_start_time_ms=metrics_start_time_ms,
2894
+ dry_run=dry_run,
2895
+ ),
2896
+ context="auto shared snapshot",
2891
2897
  )
2892
2898
  except Exception as exc:
2893
2899
  if is_rate_limit_error(exc):
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="hypertrader",
12
- version="0.1.9",
12
+ version="0.2.0",
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",
@@ -0,0 +1,127 @@
1
+ import asyncio
2
+ import unittest
3
+ from types import SimpleNamespace
4
+ from unittest.mock import AsyncMock, Mock, call, patch
5
+
6
+ import aiohttp
7
+ from hyperliquid.utils.error import ClientError, ServerError
8
+
9
+ from modes.auto_trader import get_top_perp_markets_by_volume
10
+ from utils import helpers, retry
11
+
12
+
13
+ class ApiRetryTests(unittest.IsolatedAsyncioTestCase):
14
+ async def test_repeated_gateway_errors_back_off_until_recovery(self):
15
+ read = AsyncMock(side_effect=[ServerError(502, "Bad Gateway")] * 7 + ["ok"])
16
+ with patch.object(retry.asyncio, "sleep", new_callable=AsyncMock) as sleep:
17
+ result = await retry.retry_api_read(read, context="test read")
18
+ self.assertEqual(result, "ok")
19
+ self.assertEqual(read.await_count, 8)
20
+ self.assertEqual(sleep.await_args_list, [call(n) for n in (5, 10, 20, 40, 60, 60, 60)])
21
+
22
+ async def test_backoff_resets_for_next_operation(self):
23
+ read = AsyncMock(side_effect=[ServerError(503, "unavailable"), 1] * 2)
24
+ with patch.object(retry.asyncio, "sleep", new_callable=AsyncMock) as sleep:
25
+ await retry.retry_api_read(read, context="first")
26
+ await retry.retry_api_read(read, context="second")
27
+ self.assertEqual(sleep.await_args_list, [call(5), call(5)])
28
+
29
+ async def test_non_transient_errors_propagate_without_retry(self):
30
+ for error in (ClientError(400, None, "bad request", None), ValueError("bad data")):
31
+ with self.subTest(error=error):
32
+ read = AsyncMock(side_effect=error)
33
+ with patch.object(retry.asyncio, "sleep", new_callable=AsyncMock) as sleep:
34
+ with self.assertRaises(type(error)):
35
+ await retry.retry_api_read(read, context="test read")
36
+ read.assert_awaited_once()
37
+ sleep.assert_not_awaited()
38
+
39
+ async def test_rate_limit_connection_and_timeout_errors_retry(self):
40
+ for error in (
41
+ ClientError(429, None, "rate limited", None),
42
+ aiohttp.ClientConnectionError("disconnected"),
43
+ asyncio.TimeoutError(),
44
+ ):
45
+ with self.subTest(error=error):
46
+ read = AsyncMock(side_effect=[error, "ok"])
47
+ with patch.object(retry.asyncio, "sleep", new_callable=AsyncMock) as sleep:
48
+ self.assertEqual(await retry.retry_api_read(read, context="read"), "ok")
49
+ sleep.assert_awaited_once_with(5)
50
+
51
+ async def test_cancellation_during_request_or_backoff_propagates(self):
52
+ read = AsyncMock(side_effect=asyncio.CancelledError())
53
+ with self.assertRaises(asyncio.CancelledError):
54
+ await retry.retry_api_read(read, context="cancelled read")
55
+ read.assert_awaited_once()
56
+
57
+ read = AsyncMock(side_effect=ServerError(502, "Bad Gateway"))
58
+ with patch.object(retry.asyncio, "sleep", new_callable=AsyncMock,
59
+ side_effect=asyncio.CancelledError()):
60
+ with self.assertRaises(asyncio.CancelledError):
61
+ await retry.retry_api_read(read, context="cancelled backoff")
62
+ read.assert_awaited_once()
63
+
64
+ async def test_market_ranking_recovers_from_logged_502(self):
65
+ info = SimpleNamespace(meta_and_asset_ctxs=AsyncMock(side_effect=[
66
+ ServerError(502, "<html>502 Bad Gateway</html>"),
67
+ [{"universe": [{"name": "BTC"}, {"name": "ETH"}]},
68
+ [{"dayNtlVlm": "100"}, {"dayNtlVlm": "200"}]],
69
+ ]))
70
+ with patch.object(retry.asyncio, "sleep", new_callable=AsyncMock) as sleep:
71
+ result = await get_top_perp_markets_by_volume(info, 1)
72
+ self.assertEqual(result, [("ETH", 200.0)])
73
+ sleep.assert_awaited_once_with(5)
74
+ self.assertEqual(info.meta_and_asset_ctxs.await_count, 2)
75
+
76
+ async def test_startup_closes_failed_clients_before_backoff_then_recovers(self):
77
+ for stage in ("info", "exchange", "cache"):
78
+ with self.subTest(stage=stage):
79
+ infos = [SimpleNamespace(initialize=AsyncMock(), aclose=AsyncMock()) for _ in range(2)]
80
+ exchanges = [SimpleNamespace(initialize=AsyncMock(), aclose=AsyncMock()) for _ in range(2)]
81
+ caches = [SimpleNamespace(start=AsyncMock(), stop=AsyncMock()) for _ in range(2)]
82
+ failure = ServerError(502, "Bad Gateway")
83
+ if stage == "info":
84
+ infos[0].initialize.side_effect = failure
85
+ elif stage == "exchange":
86
+ exchanges[0].initialize.side_effect = failure
87
+ else:
88
+ caches[0].start.side_effect = failure
89
+
90
+ async def check_cleanup(delay):
91
+ self.assertEqual(delay, 5)
92
+ infos[0].aclose.assert_awaited_once()
93
+ if stage != "info":
94
+ exchanges[0].aclose.assert_awaited_once()
95
+ if stage == "cache":
96
+ caches[0].stop.assert_awaited_once()
97
+
98
+ with (
99
+ patch.object(helpers, "load_credentials", return_value=("secret", "0xabc")),
100
+ patch.object(helpers.eth_account.Account, "from_key", return_value=object()),
101
+ patch.object(helpers, "Info", side_effect=infos),
102
+ patch.object(helpers, "Exchange", side_effect=exchanges),
103
+ patch.object(helpers, "HyperliquidWebsocketCache", side_effect=caches),
104
+ patch.object(helpers, "get_ws_cache", side_effect=lambda info: getattr(info, "ws_cache", None)),
105
+ patch.object(retry.asyncio, "sleep", new_callable=AsyncMock, side_effect=check_cleanup),
106
+ ):
107
+ address, info, exchange = await helpers.init_clients(False, use_websocket=False)
108
+ self.assertEqual(address, "0xabc")
109
+ self.assertIs(info, infos[1])
110
+ self.assertIs(exchange, exchanges[0 if stage == "info" else 1])
111
+ info.aclose.assert_not_awaited()
112
+ exchange.aclose.assert_not_awaited()
113
+
114
+ async def test_startup_cancellation_closes_partial_client(self):
115
+ info = SimpleNamespace(initialize=AsyncMock(side_effect=asyncio.CancelledError()), aclose=AsyncMock())
116
+ with (
117
+ patch.object(helpers, "load_credentials", return_value=("secret", "0xabc")),
118
+ patch.object(helpers.eth_account.Account, "from_key", return_value=object()),
119
+ patch.object(helpers, "Info", return_value=info),
120
+ patch.object(helpers, "Exchange", new_callable=Mock) as exchange,
121
+ patch.object(retry.asyncio, "sleep", new_callable=AsyncMock) as sleep,
122
+ ):
123
+ with self.assertRaises(asyncio.CancelledError):
124
+ await helpers.init_clients(False)
125
+ info.aclose.assert_awaited_once()
126
+ exchange.assert_not_called()
127
+ sleep.assert_not_awaited()
@@ -0,0 +1,86 @@
1
+ import subprocess
2
+ import sys
3
+ import textwrap
4
+ import unittest
5
+
6
+
7
+ # Run SIGINT in a child process so it cannot interrupt the test runner.
8
+ SHUTDOWN_SCRIPT = textwrap.dedent("""\
9
+ import asyncio
10
+ import gc
11
+ import signal
12
+ import sys
13
+ from unittest.mock import patch
14
+
15
+ import aiohttp
16
+ import hypertrader
17
+
18
+ backend, scenario = sys.argv[1:]
19
+ if backend == "uvloop":
20
+ import uvloop
21
+ uvloop.install()
22
+
23
+ loops = []
24
+ sessions = []
25
+ tasks = []
26
+ events = []
27
+ errors = []
28
+
29
+ async def background(attempt):
30
+ try:
31
+ await asyncio.Event().wait()
32
+ finally:
33
+ await asyncio.sleep(0)
34
+ events.append((attempt, "background closed"))
35
+
36
+ async def application(argv):
37
+ attempt = len(loops)
38
+ loop = asyncio.get_running_loop()
39
+ loops.append(loop)
40
+ loop.set_exception_handler(lambda loop, context: errors.append(context))
41
+ session = aiohttp.ClientSession()
42
+ sessions.append(session)
43
+ tasks.append(asyncio.create_task(background(attempt)))
44
+ try:
45
+ await asyncio.sleep(0)
46
+ if scenario.startswith("restart") and attempt == 0:
47
+ raise RuntimeError("trigger restart")
48
+ if scenario.endswith("interrupt"):
49
+ loop.call_soon(signal.raise_signal, signal.SIGINT)
50
+ await asyncio.Event().wait()
51
+ finally:
52
+ await asyncio.sleep(0)
53
+ await session.close()
54
+ events.append((attempt, "session closed"))
55
+
56
+ with patch.object(hypertrader, "async_main", application), patch.object(
57
+ hypertrader, "configure_runtime_logging", return_value="unused.log"
58
+ ):
59
+ hypertrader.main([])
60
+
61
+ gc.collect()
62
+ assert len(loops) == (2 if scenario.startswith("restart") else 1), events
63
+ assert all(loop.is_closed() for loop in loops), "event loop left open"
64
+ assert all(session.closed for session in sessions), "HTTP session left open"
65
+ assert all(task.done() for task in tasks), "background task left pending"
66
+ for attempt in range(len(loops)):
67
+ assert (attempt, "session closed") in events, events
68
+ assert (attempt, "background closed") in events, events
69
+ assert not errors, errors
70
+ print("shutdown verified")
71
+ """)
72
+
73
+
74
+ class CliShutdownTests(unittest.TestCase):
75
+ def test_shutdown_and_restart_release_async_resources(self):
76
+ for backend in ("asyncio", "uvloop"):
77
+ for scenario in ("normal", "interrupt", "restart", "restart_interrupt"):
78
+ with self.subTest(backend=backend, scenario=scenario):
79
+ result = subprocess.run(
80
+ [sys.executable, "-c", SHUTDOWN_SCRIPT, backend, scenario],
81
+ capture_output=True, text=True, timeout=20,
82
+ )
83
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
84
+ self.assertIn("shutdown verified", result.stdout)
85
+ self.assertNotIn("Unclosed", result.stderr)
86
+ self.assertNotIn("Task was destroyed", result.stderr)
@@ -15,6 +15,7 @@ from hyperliquid.utils import constants
15
15
 
16
16
  from utils.constants import INTERVAL_TO_MS, WATCH_RETRY_SLEEP_SECONDS
17
17
  from utils.worker import AsyncTaskQueue
18
+ from utils.retry import retry_api_read
18
19
 
19
20
  decimal.getcontext().prec = 4
20
21
 
@@ -698,13 +699,24 @@ async def init_clients(use_testnet: bool, use_websocket: bool = True) -> Tuple[s
698
699
  api_url = constants.TESTNET_API_URL if use_testnet else constants.MAINNET_API_URL
699
700
  account: LocalAccount = eth_account.Account.from_key(secret_key)
700
701
 
701
- info = await Info.create(api_url, skip_ws=(not use_websocket))
702
- exchange = await Exchange.create(account, api_url, account_address=account_address)
702
+ async def initialize() -> Tuple[str, Info, Exchange]:
703
+ # Keep references before initialization so failed SDK requests cannot
704
+ # strand HTTP sessions or websockets inside the SDK's create() methods.
705
+ info = Info(api_url, skip_ws=(not use_websocket))
706
+ exchange = None
707
+ try:
708
+ await info.initialize()
709
+ exchange = Exchange(account, api_url, account_address=account_address)
710
+ await exchange.initialize()
711
+ ws_cache = HyperliquidWebsocketCache(info, account_address, enabled=use_websocket)
712
+ setattr(info, "ws_cache", ws_cache)
713
+ await ws_cache.start()
714
+ return account_address, info, exchange
715
+ except BaseException:
716
+ await close_clients(info, exchange)
717
+ raise
703
718
 
704
- ws_cache = HyperliquidWebsocketCache(info, account_address, enabled=use_websocket)
705
- setattr(info, "ws_cache", ws_cache)
706
- await ws_cache.start()
707
- return account_address, info, exchange
719
+ return await retry_api_read(initialize, context="Hyperliquid client initialization")
708
720
 
709
721
 
710
722
  async def close_clients(info: Optional[Info], exchange: Optional[Exchange]) -> None:
@@ -0,0 +1,41 @@
1
+ """Backoff for read-only requests and client initialization."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Awaitable, Callable, TypeVar
6
+
7
+ import aiohttp
8
+ from hyperliquid.utils.error import ClientError, ServerError
9
+
10
+
11
+ T = TypeVar("T")
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def is_transient_api_error(exc: Exception) -> bool:
16
+ if isinstance(exc, (ClientError, ServerError)):
17
+ return exc.status_code == 429 or 500 <= exc.status_code < 600
18
+ if isinstance(exc, aiohttp.ClientResponseError):
19
+ return exc.status == 429 or 500 <= exc.status < 600
20
+ return isinstance(exc, (aiohttp.ClientConnectionError, asyncio.TimeoutError))
21
+
22
+
23
+ async def retry_api_read(operation: Callable[[], Awaitable[T]], *, context: str) -> T:
24
+ """Retry transient failures until recovery or cancellation; never use for orders."""
25
+ delay = 5.0
26
+ while True:
27
+ try:
28
+ return await operation()
29
+ except Exception as exc:
30
+ if not is_transient_api_error(exc):
31
+ raise
32
+ status = getattr(exc, "status_code", getattr(exc, "status", None))
33
+ message = (
34
+ f"[API-RETRY] {context}: {type(exc).__name__}"
35
+ f"{f' HTTP {status}' if status is not None else ''}. "
36
+ f"Sleeping {delay:.1f}s before retry."
37
+ )
38
+ print(message)
39
+ logger.warning(message)
40
+ await asyncio.sleep(delay)
41
+ delay = min(delay * 2, 60.0)
File without changes
File without changes
File without changes
File without changes