hyperliquid-cli-python 0.1.0__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.
@@ -0,0 +1,814 @@
1
+ import argparse
2
+ import asyncio
3
+ import json
4
+ import sys
5
+ import time
6
+ from textwrap import dedent
7
+ from types import SimpleNamespace
8
+ from typing import Any, Callable, Optional
9
+
10
+ from ..core.context import CLIContext, load_config
11
+ from ..commands import app as legacy
12
+
13
+
14
+ TOP_LEVEL_COMMANDS = ["account", "order", "asset", "markets", "referral", "completion"]
15
+ SUBCOMMANDS: dict[str, list[str]] = {
16
+ "account": ["add", "ls", "set-default", "remove", "positions", "orders", "balances", "portfolio"],
17
+ "order": ["ls", "limit", "market", "tpsl", "twap", "twap-cancel", "cancel", "cancel-all", "set-leverage", "configure"],
18
+ "asset": ["price", "book", "leverage"],
19
+ "markets": ["ls", "search"],
20
+ "referral": ["set", "status"],
21
+ "completion": ["bash"],
22
+ }
23
+ GLOBAL_OPTIONS = ["--json", "--testnet", "-h", "--help"]
24
+
25
+
26
+ def _ctx(json_output: bool, testnet: bool) -> SimpleNamespace:
27
+ return SimpleNamespace(
28
+ obj={
29
+ "context": CLIContext(load_config(testnet)),
30
+ "json": json_output,
31
+ "start": time.perf_counter(),
32
+ }
33
+ )
34
+
35
+
36
+ def _exit_with_error(msg: str, code: int = 2) -> None:
37
+ print(msg, file=sys.stderr)
38
+ raise SystemExit(code)
39
+
40
+
41
+ def _bash_completion_script() -> str:
42
+ top_level = " ".join(TOP_LEVEL_COMMANDS)
43
+ global_options = " ".join(GLOBAL_OPTIONS)
44
+ case_lines = "\n".join(
45
+ [f' {name}) COMPREPLY=( $(compgen -W "{ " ".join(values) }" -- "$cur") ) ;;' for name, values in SUBCOMMANDS.items()]
46
+ )
47
+ return dedent(
48
+ f"""\
49
+ # bash completion for hl
50
+ _hl_completion() {{
51
+ local cur prev cmd i
52
+ COMPREPLY=()
53
+ cur="${{COMP_WORDS[COMP_CWORD]}}"
54
+ prev="${{COMP_WORDS[COMP_CWORD-1]}}"
55
+ cmd=""
56
+
57
+ for ((i=1; i < COMP_CWORD; i++)); do
58
+ case "${{COMP_WORDS[i]}}" in
59
+ -*) ;;
60
+ *)
61
+ cmd="${{COMP_WORDS[i]}}"
62
+ break
63
+ ;;
64
+ esac
65
+ done
66
+
67
+ if [[ "$cur" == -* ]]; then
68
+ COMPREPLY=( $(compgen -W "{global_options}" -- "$cur") )
69
+ return 0
70
+ fi
71
+
72
+ if [[ -z "$cmd" ]]; then
73
+ COMPREPLY=( $(compgen -W "{top_level}" -- "$cur") )
74
+ return 0
75
+ fi
76
+
77
+ case "$cmd" in
78
+ {case_lines}
79
+ *)
80
+ COMPREPLY=()
81
+ ;;
82
+ esac
83
+ }}
84
+
85
+ complete -F _hl_completion hl
86
+ """
87
+ )
88
+
89
+
90
+ def _print_completion(shell: str) -> None:
91
+ if shell != "bash":
92
+ _exit_with_error(f"Unsupported shell: {shell}")
93
+ print(_bash_completion_script(), end="")
94
+
95
+
96
+ def _parse_limit_shape(args: argparse.Namespace) -> tuple[str, str, str]:
97
+ # Normal mode: hl order limit <side> <size> <coin> <price>
98
+ # Stake mode: hl order limit <side> <coin> <price> --stake <usd>
99
+ a = args.a
100
+ b = args.b
101
+ c = args.c
102
+ stake = args.stake
103
+ if a is None or b is None:
104
+ _exit_with_error("Missing arguments. See: hl order limit -h")
105
+
106
+ if stake is not None:
107
+ if c is not None:
108
+ _exit_with_error("When --stake is used, syntax is: hl order limit <side> <coin> <price> --stake <usd>")
109
+ try:
110
+ px = float(b)
111
+ except ValueError as exc:
112
+ raise SystemExit(f"Invalid price: {b}") from exc
113
+ if px <= 0:
114
+ _exit_with_error("price must be positive")
115
+ if float(stake) <= 0:
116
+ _exit_with_error("stake must be positive")
117
+ derived_size = str(float(stake) / px)
118
+ return derived_size, a, b
119
+
120
+ if c is None:
121
+ _exit_with_error("Missing price. Syntax: hl order limit <side> <size> <coin> <price>")
122
+ return a, b, c
123
+
124
+
125
+ def _parse_market_shape(args: argparse.Namespace) -> tuple[str, str]:
126
+ # Normal mode: hl order market <side> <size> <coin>
127
+ # Stake mode: hl order market <side> <coin> --stake <usd>
128
+ a = args.a
129
+ b = args.b
130
+ stake = args.stake
131
+ if a is None:
132
+ _exit_with_error("Missing arguments. See: hl order market -h")
133
+
134
+ if stake is not None:
135
+ if b is not None:
136
+ _exit_with_error("When --stake is used, syntax is: hl order market <side> <coin> --stake <usd>")
137
+ if float(stake) <= 0:
138
+ _exit_with_error("stake must be positive")
139
+ return "0", a
140
+
141
+ if b is None:
142
+ _exit_with_error("Missing coin. Syntax: hl order market <side> <size> <coin>")
143
+ return a, b
144
+
145
+
146
+ def _build_parser() -> argparse.ArgumentParser:
147
+ epilog = (
148
+ "Command tree:\n"
149
+ " account add|ls|set-default|remove|positions|orders|balances|portfolio\n"
150
+ " order ls|limit|market|tpsl|twap|twap-cancel|cancel|cancel-all|set-leverage|configure\n"
151
+ " asset price|book|leverage\n"
152
+ " markets ls\n"
153
+ " referral set|status\n"
154
+ "Examples:\n"
155
+ " hl account add\n"
156
+ " hl order twap buy 1 BTC 30 --randomize\n"
157
+ " hl order twap-cancel BTC 12345\n"
158
+ " hl account positions --watch\n"
159
+ )
160
+ p = argparse.ArgumentParser(
161
+ prog="hl",
162
+ description="CLI for Hyperliquid DEX",
163
+ formatter_class=argparse.RawTextHelpFormatter,
164
+ epilog=epilog,
165
+ )
166
+ p.add_argument("--json", action="store_true", help="Output in JSON format")
167
+ p.add_argument("--testnet", action="store_true", help="Use testnet")
168
+
169
+ sub = p.add_subparsers(dest="command")
170
+
171
+ def add_cmd_parser(
172
+ subparsers: Any,
173
+ name: str,
174
+ help_text: str,
175
+ examples: list[str] | None = None,
176
+ ) -> argparse.ArgumentParser:
177
+ ep = None
178
+ if examples:
179
+ ep = "Examples:\n" + "\n".join([f" {x}" for x in examples])
180
+ return subparsers.add_parser(
181
+ name,
182
+ help=help_text,
183
+ formatter_class=argparse.RawTextHelpFormatter,
184
+ epilog=ep,
185
+ )
186
+
187
+ # account
188
+ account = add_cmd_parser(
189
+ sub,
190
+ "account",
191
+ "Account management and information",
192
+ ["hl account add", "hl account ls", "hl account positions --watch"],
193
+ )
194
+ acc_sub = account.add_subparsers(dest="account_command")
195
+ add_cmd_parser(acc_sub, "add", "Add account", ["hl account add"])
196
+ add_cmd_parser(acc_sub, "ls", "List accounts", ["hl account ls", "hl --json account ls"])
197
+ acc_set = add_cmd_parser(acc_sub, "set-default", "Set default account", ["hl account set-default main"])
198
+ acc_set.add_argument("alias")
199
+ acc_rm = add_cmd_parser(acc_sub, "remove", "Remove account", ["hl account remove main", "hl account remove main --force"])
200
+ acc_rm.add_argument("alias")
201
+ acc_rm.add_argument("-f", "--force", action="store_true")
202
+
203
+ for name, help_text in [
204
+ ("positions", "Get positions"),
205
+ ("orders", "Get orders"),
206
+ ("balances", "Get balances"),
207
+ ("portfolio", "Get portfolio"),
208
+ ]:
209
+ s = add_cmd_parser(
210
+ acc_sub,
211
+ name,
212
+ help_text,
213
+ [
214
+ f"hl account {name}",
215
+ f"hl account {name} --watch",
216
+ f"hl account {name} --user 0x1234567890abcdef1234567890abcdef12345678",
217
+ ],
218
+ )
219
+ s.add_argument("--user")
220
+ s.add_argument("-w", "--watch", action="store_true")
221
+
222
+ # order
223
+ order = add_cmd_parser(
224
+ sub,
225
+ "order",
226
+ "Order management and trading",
227
+ ["hl order ls", "hl order limit buy 0.01 BTC 60000", "hl order limit buy BTC 65000 --stake 50", "hl order twap sell 1 BTC 30"],
228
+ )
229
+ ord_sub = order.add_subparsers(dest="order_command")
230
+ ord_ls = add_cmd_parser(ord_sub, "ls", "List open orders", ["hl order ls", "hl order ls --watch"])
231
+ ord_ls.add_argument("--user")
232
+ ord_ls.add_argument("-w", "--watch", action="store_true")
233
+
234
+ ord_limit = add_cmd_parser(
235
+ ord_sub,
236
+ "limit",
237
+ "Place limit order",
238
+ [
239
+ "hl order limit buy 0.001 BTC 65000",
240
+ "hl order limit buy BTC 65000 --stake 50 # size is derived from about $50 notional when --leverage is omitted",
241
+ "hl order limit buy BTC 65000 --stake 50",
242
+ "hl order limit buy BTC 65000 --stake 50 --leverage 20 --cross # about $1000 position notional",
243
+ "hl order limit sell 0.1 ETH 3500 --tif Gtc",
244
+ "hl order limit long 1 SOL 100 --reduce-only",
245
+ ],
246
+ )
247
+ ord_limit.add_argument("side")
248
+ ord_limit.add_argument("a", nargs="?")
249
+ ord_limit.add_argument("b", nargs="?")
250
+ ord_limit.add_argument("c", nargs="?")
251
+ ord_limit.add_argument("--tif", default="Gtc")
252
+ ord_limit.add_argument("--reduce-only", action="store_true")
253
+ ord_limit.add_argument("--stake", type=float, help="USD margin used to derive order size. With --leverage, size uses stake x leverage; without it, size uses stake only")
254
+ ord_limit.add_argument("--leverage", type=int, help="Optional leverage update before placing the order. If omitted, the CLI does not multiply stake by leverage for size calculation")
255
+ ord_limit.add_argument("--cross", action="store_true", help="Use cross margin with --leverage")
256
+ ord_limit.add_argument("--isolated", action="store_true", help="Use isolated margin with --leverage")
257
+
258
+ ord_market = add_cmd_parser(
259
+ ord_sub,
260
+ "market",
261
+ "Place market order",
262
+ [
263
+ "hl order market buy 0.001 BTC",
264
+ "hl order market buy BTC --stake 50 # size is derived from about $50 notional when --leverage is omitted",
265
+ "hl order market buy BTC --stake 50 --leverage 20 --cross # about $1000 position notional",
266
+ "hl order market sell 0.1 ETH --slippage 0.5",
267
+ "hl order market close ETH",
268
+ "hl order market close xyz:TSLA",
269
+ "hl order market close ETH --ratio 0.5",
270
+ ],
271
+ )
272
+ ord_market.add_argument("side")
273
+ ord_market.add_argument("a", nargs="?")
274
+ ord_market.add_argument("b", nargs="?")
275
+ ord_market.add_argument("--reduce-only", action="store_true")
276
+ ord_market.add_argument("--slippage", type=float)
277
+ ord_market.add_argument("--stake", type=float, help="USD margin used to derive order size. With --leverage, size uses stake x leverage; without it, size uses stake only")
278
+ ord_market.add_argument("--leverage", type=int, help="Optional leverage update before placing the order. If omitted, the CLI does not multiply stake by leverage for size calculation")
279
+ ord_market.add_argument("--cross", action="store_true", help="Use cross margin with --leverage")
280
+ ord_market.add_argument("--isolated", action="store_true", help="Use isolated margin with --leverage")
281
+ ord_market.add_argument("--ratio", type=float, default=1.0, help="Close ratio (0 < ratio <= 1) for market close")
282
+
283
+ ord_twap = add_cmd_parser(
284
+ ord_sub,
285
+ "twap",
286
+ "Place TWAP order",
287
+ [
288
+ "hl order twap buy 1 BTC 30",
289
+ "hl order twap buy 0 BTC 30 --stake 5 # size is derived from about $5 total notional when --leverage is omitted",
290
+ "hl order twap buy 0 BTC 30 --stake 5 --leverage 20 --cross # about $100 total notional",
291
+ "hl order twap sell 2 ETH 5,10 --randomize",
292
+ "hl order twap sell 1 BTC 30 --leverage 20 --isolated",
293
+ "hl order twap sell 1 BTC 30 --reduce-only",
294
+ ],
295
+ )
296
+ ord_twap.add_argument("side")
297
+ ord_twap.add_argument("size")
298
+ ord_twap.add_argument("coin")
299
+ ord_twap.add_argument("interval")
300
+ ord_twap.add_argument("--stake", type=float, help="USD margin used to derive total TWAP size. With --leverage, size uses stake x leverage; without it, size uses stake only")
301
+ ord_twap.add_argument("--reduce-only", action="store_true")
302
+ ord_twap.add_argument("--randomize", action="store_true")
303
+ ord_twap.add_argument("--leverage", type=int, help="Optional leverage update before placing the order. If omitted, the CLI does not multiply stake by leverage for size calculation")
304
+ ord_twap.add_argument("--cross", action="store_true", help="Use cross margin with --leverage")
305
+ ord_twap.add_argument("--isolated", action="store_true", help="Use isolated margin with --leverage")
306
+
307
+ ord_tpsl = add_cmd_parser(
308
+ ord_sub,
309
+ "tpsl",
310
+ "Set TP/SL trigger orders for an open position",
311
+ [
312
+ "hl order tpsl ETH --tp 1900 --sl 1800",
313
+ "hl order tpsl ETH --sl 1800 --ratio 0.5",
314
+ "hl order tpsl xyz:TSLA --tp 420",
315
+ ],
316
+ )
317
+ ord_tpsl.add_argument("coin")
318
+ ord_tpsl.add_argument("--tp", type=float, help="Take-profit trigger price")
319
+ ord_tpsl.add_argument("--sl", type=float, help="Stop-loss trigger price")
320
+ ord_tpsl.add_argument("--ratio", type=float, default=1.0, help="Position ratio to protect (0 < ratio <= 1)")
321
+
322
+ ord_twap_cancel = add_cmd_parser(
323
+ ord_sub,
324
+ "twap-cancel",
325
+ "Cancel native TWAP order",
326
+ ["hl order twap-cancel BTC 12345"],
327
+ )
328
+ ord_twap_cancel.add_argument("coin")
329
+ ord_twap_cancel.add_argument("twap_id")
330
+
331
+ ord_cancel = add_cmd_parser(
332
+ ord_sub,
333
+ "cancel",
334
+ "Cancel order",
335
+ ["hl order cancel 123456", "hl order cancel"],
336
+ )
337
+ ord_cancel.add_argument("oid", nargs="?")
338
+
339
+ ord_cancel_all = add_cmd_parser(
340
+ ord_sub,
341
+ "cancel-all",
342
+ "Cancel all orders",
343
+ ["hl order cancel-all", "hl order cancel-all --coin BTC -y"],
344
+ )
345
+ ord_cancel_all.add_argument("-y", "--yes", action="store_true")
346
+ ord_cancel_all.add_argument("--coin")
347
+
348
+ ord_lev = add_cmd_parser(
349
+ ord_sub,
350
+ "set-leverage",
351
+ "Set leverage",
352
+ ["hl order set-leverage BTC 10 --cross", "hl order set-leverage ETH 5 --isolated"],
353
+ )
354
+ ord_lev.add_argument("coin")
355
+ ord_lev.add_argument("leverage")
356
+ ord_lev.add_argument("--cross", action="store_true")
357
+ ord_lev.add_argument("--isolated", action="store_true")
358
+
359
+ ord_cfg = add_cmd_parser(
360
+ ord_sub,
361
+ "configure",
362
+ "Configure order defaults",
363
+ ["hl order configure", "hl order configure --slippage 0.8"],
364
+ )
365
+ ord_cfg.add_argument("--slippage", type=float)
366
+
367
+ # asset
368
+ asset = add_cmd_parser(
369
+ sub,
370
+ "asset",
371
+ "Asset-specific information",
372
+ ["hl asset price BTC", "hl asset book ETH --watch", "hl asset leverage BTC"],
373
+ )
374
+ as_sub = asset.add_subparsers(dest="asset_command")
375
+ as_price = add_cmd_parser(as_sub, "price", "Get price", ["hl asset price BTC", "hl asset price BTC --watch"])
376
+ as_price.add_argument("coin")
377
+ as_price.add_argument("-w", "--watch", action="store_true")
378
+
379
+ as_book = add_cmd_parser(as_sub, "book", "Get orderbook", ["hl asset book BTC", "hl asset book ETH --watch"])
380
+ as_book.add_argument("coin")
381
+ as_book.add_argument("-w", "--watch", action="store_true")
382
+
383
+ as_lev = add_cmd_parser(
384
+ as_sub,
385
+ "leverage",
386
+ "Get leverage info",
387
+ [
388
+ "hl asset leverage BTC",
389
+ "hl asset leverage ETH --user 0x1234567890abcdef1234567890abcdef12345678",
390
+ "hl asset leverage BTC --watch",
391
+ ],
392
+ )
393
+ as_lev.add_argument("coin")
394
+ as_lev.add_argument("--user")
395
+ as_lev.add_argument("-w", "--watch", action="store_true")
396
+
397
+ # markets
398
+ markets = add_cmd_parser(
399
+ sub,
400
+ "markets",
401
+ "Market information",
402
+ ["hl markets ls", "hl markets search ORCL", "hl markets search xyz"],
403
+ )
404
+ mk_sub = markets.add_subparsers(dest="markets_command")
405
+ mk_ls = add_cmd_parser(
406
+ mk_sub,
407
+ "ls",
408
+ "List markets",
409
+ ["hl markets ls", "hl markets ls --spot-only", "hl markets ls --perp-only --watch"],
410
+ )
411
+ mk_ls.add_argument("--spot-only", action="store_true")
412
+ mk_ls.add_argument("--perp-only", action="store_true")
413
+ mk_ls.add_argument(
414
+ "--category",
415
+ nargs="?",
416
+ const="*",
417
+ help="Filter perp markets by category (e.g. stocks, commodities, indices, fx, preipo, crypto)",
418
+ )
419
+ mk_ls.add_argument(
420
+ "--sort-by",
421
+ default="volume",
422
+ help="Sort markets by volume, oi, price, change, funding, or coin",
423
+ )
424
+ mk_ls.add_argument("-w", "--watch", action="store_true")
425
+ mk_search = add_cmd_parser(
426
+ mk_sub,
427
+ "search",
428
+ "Search markets by partial symbol/name",
429
+ ["hl markets search ORCL", "hl markets search xyz", "hl markets search TSLA --perp-only"],
430
+ )
431
+ mk_search.add_argument("query")
432
+ mk_search.add_argument("--spot-only", action="store_true")
433
+ mk_search.add_argument("--perp-only", action="store_true")
434
+ mk_search.add_argument(
435
+ "--category",
436
+ nargs="?",
437
+ const="*",
438
+ help="Filter perp markets by category (e.g. stocks, commodities, indices, fx, preipo, crypto)",
439
+ )
440
+ mk_search.add_argument(
441
+ "--sort-by",
442
+ default="volume",
443
+ help="Sort matches by volume, oi, price, change, funding, or coin",
444
+ )
445
+
446
+ # referral
447
+ referral = add_cmd_parser(
448
+ sub,
449
+ "referral",
450
+ "Referral management",
451
+ ["hl referral set MYCODE", "hl referral status"],
452
+ )
453
+ rf_sub = referral.add_subparsers(dest="referral_command")
454
+ rf_set = add_cmd_parser(rf_sub, "set", "Set referral code", ["hl referral set MYCODE"])
455
+ rf_set.add_argument("code")
456
+ add_cmd_parser(rf_sub, "status", "Get referral status", ["hl referral status"])
457
+
458
+ completion = add_cmd_parser(
459
+ sub,
460
+ "completion",
461
+ "Print shell completion script",
462
+ ['eval "$(hl completion bash)"'],
463
+ )
464
+ completion_sub = completion.add_subparsers(dest="completion_command")
465
+ add_cmd_parser(completion_sub, "bash", "Print bash completion script", ['eval "$(hl completion bash)"'])
466
+
467
+ return p
468
+
469
+
470
+ async def _call(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
471
+ try:
472
+ if asyncio.iscoroutinefunction(fn):
473
+ await fn(*args, **kwargs)
474
+ else:
475
+ fn(*args, **kwargs)
476
+ except SystemExit:
477
+ raise
478
+ except BaseException as exc: # noqa: BLE001
479
+ _exit_with_error(str(exc), 1)
480
+
481
+
482
+ async def _fetch_balances_async(context: CLIContext, user: str) -> dict[str, Any]:
483
+ info = context.get_public_client()
484
+ perp_task = asyncio.to_thread(info.user_state, user)
485
+ spot_task = asyncio.to_thread(info.spot_user_state, user)
486
+ perp, spot = await asyncio.gather(perp_task, spot_task)
487
+
488
+ balances = []
489
+ for b in spot["balances"]:
490
+ if float(b["total"]) == 0:
491
+ continue
492
+ total = float(b["total"])
493
+ hold = float(b["hold"])
494
+ balances.append(
495
+ {
496
+ "token": b["coin"],
497
+ "total": b["total"],
498
+ "hold": b["hold"],
499
+ "available": f"{total - hold}",
500
+ }
501
+ )
502
+ return {"spotBalances": balances, "perpBalance": perp["marginSummary"]["accountValue"]}
503
+
504
+
505
+ async def _account_balances_async(ctx: SimpleNamespace, *, user: Optional[str], watch: bool) -> None:
506
+ context = legacy._ctx(ctx)
507
+ address = legacy.validate_address(user) if user else context.get_wallet_address()
508
+
509
+ if not watch:
510
+ data = await _fetch_balances_async(context, address)
511
+ legacy.out(data, legacy._json(ctx))
512
+ legacy._done(ctx)
513
+ return
514
+
515
+ try:
516
+ while True:
517
+ data = await _fetch_balances_async(context, address)
518
+ if legacy._json(ctx):
519
+ print(json.dumps(data, ensure_ascii=False))
520
+ else:
521
+ legacy.console.clear()
522
+ legacy._render_table(
523
+ f"Balances (Perp USD: {data['perpBalance']})",
524
+ ["Token", "Total", "Hold", "Available"],
525
+ [[b["token"], b["total"], b["hold"], b["available"]] for b in data["spotBalances"]],
526
+ )
527
+ await asyncio.sleep(1.0)
528
+ except KeyboardInterrupt:
529
+ return
530
+
531
+
532
+ async def _fetch_portfolio_async(context: CLIContext, user: str) -> dict[str, Any]:
533
+ info = context.get_public_client()
534
+ perp_tasks = [
535
+ asyncio.to_thread(info.user_state, user, dex)
536
+ for dex in context.get_perp_dexs()
537
+ ]
538
+ spot_task = asyncio.to_thread(info.spot_user_state, user)
539
+ *perp_states, spot = await asyncio.gather(*perp_tasks, spot_task)
540
+
541
+ positions = []
542
+ for state in perp_states:
543
+ positions.extend(
544
+ [
545
+ {
546
+ "coin": p["position"]["coin"],
547
+ "size": p["position"]["szi"],
548
+ "entryPx": p["position"].get("entryPx"),
549
+ "positionValue": p["position"].get("positionValue"),
550
+ "unrealizedPnl": p["position"].get("unrealizedPnl"),
551
+ "leverage": f"{p['position']['leverage']['value']}x {p['position']['leverage']['type']}",
552
+ "liquidationPx": p["position"].get("liquidationPx") or "-",
553
+ }
554
+ for p in state["assetPositions"]
555
+ if float(p["position"]["szi"]) != 0
556
+ ]
557
+ )
558
+ spot_balances = []
559
+ for b in spot["balances"]:
560
+ if float(b["total"]) == 0:
561
+ continue
562
+ total = float(b["total"])
563
+ hold = float(b["hold"])
564
+ spot_balances.append(
565
+ {
566
+ "token": b["coin"],
567
+ "total": b["total"],
568
+ "hold": b["hold"],
569
+ "available": f"{total - hold}",
570
+ }
571
+ )
572
+ account_value = sum(float(s["marginSummary"]["accountValue"]) for s in perp_states)
573
+ margin_used = sum(float(s["marginSummary"]["totalMarginUsed"]) for s in perp_states)
574
+ return {
575
+ "positions": positions,
576
+ "spotBalances": spot_balances,
577
+ "accountValue": f"{account_value:.8f}",
578
+ "totalMarginUsed": f"{margin_used:.8f}",
579
+ }
580
+
581
+
582
+ async def _account_portfolio_async(ctx: SimpleNamespace, *, user: Optional[str], watch: bool) -> None:
583
+ context = legacy._ctx(ctx)
584
+ address = legacy.validate_address(user) if user else context.get_wallet_address()
585
+
586
+ if not watch:
587
+ data = await _fetch_portfolio_async(context, address)
588
+ legacy.out(data, legacy._json(ctx))
589
+ legacy._done(ctx)
590
+ return
591
+
592
+ try:
593
+ while True:
594
+ data = await _fetch_portfolio_async(context, address)
595
+ if legacy._json(ctx):
596
+ print(json.dumps(data, ensure_ascii=False))
597
+ else:
598
+ legacy.console.clear()
599
+ legacy._render_table(
600
+ f"Portfolio AccountValue={data['accountValue']} MarginUsed={data['totalMarginUsed']}",
601
+ ["Coin", "Size", "Entry", "Value", "PnL", "Leverage"],
602
+ [
603
+ [p["coin"], p["size"], p["entryPx"], p["positionValue"], p["unrealizedPnl"], p["leverage"]]
604
+ for p in data["positions"]
605
+ ],
606
+ )
607
+ legacy._render_table(
608
+ "Spot Balances",
609
+ ["Token", "Total", "Hold", "Available"],
610
+ [[b["token"], b["total"], b["hold"], b["available"]] for b in data["spotBalances"]],
611
+ )
612
+ await asyncio.sleep(1.0)
613
+ except KeyboardInterrupt:
614
+ return
615
+
616
+
617
+ async def dispatch(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None:
618
+ ctx = _ctx(args.json, args.testnet)
619
+
620
+ cmd = args.command
621
+ if cmd is None:
622
+ parser.print_help()
623
+ raise SystemExit(0)
624
+
625
+ if cmd == "account":
626
+ sc = args.account_command
627
+ if sc is None:
628
+ legacy._print_account_add_guide()
629
+ return
630
+ if sc == "add":
631
+ await _call(legacy.account_add, ctx)
632
+ elif sc == "ls":
633
+ await _call(legacy.account_ls, ctx)
634
+ elif sc == "set-default":
635
+ await _call(legacy.account_set_default, ctx, args.alias)
636
+ elif sc == "remove":
637
+ await _call(legacy.account_remove, ctx, args.alias, args.force)
638
+ elif sc == "positions":
639
+ await _call(legacy.account_positions, ctx, user=args.user, watch=args.watch)
640
+ elif sc == "orders":
641
+ await _call(legacy.account_orders, ctx, user=args.user, watch=args.watch)
642
+ elif sc == "balances":
643
+ await _call(_account_balances_async, ctx, user=args.user, watch=args.watch)
644
+ elif sc == "portfolio":
645
+ await _call(_account_portfolio_async, ctx, user=args.user, watch=args.watch)
646
+ else:
647
+ _exit_with_error(f"Unknown account subcommand: {sc}")
648
+ return
649
+
650
+ if cmd == "order":
651
+ sc = args.order_command
652
+ if sc is None:
653
+ _exit_with_error("Missing order subcommand. Run: hl order -h")
654
+ if sc == "ls":
655
+ await _call(legacy.order_ls, ctx, user=args.user, watch=args.watch)
656
+ elif sc == "limit":
657
+ size, coin, price = _parse_limit_shape(args)
658
+ await _call(
659
+ legacy.order_limit,
660
+ ctx,
661
+ args.side,
662
+ size,
663
+ coin,
664
+ price,
665
+ tif=args.tif,
666
+ reduce_only=args.reduce_only,
667
+ stake=args.stake,
668
+ leverage=args.leverage,
669
+ cross=args.cross,
670
+ isolated=args.isolated,
671
+ )
672
+ elif sc == "market":
673
+ if str(args.side).lower() == "close":
674
+ if args.a is None or args.b is not None:
675
+ _exit_with_error("Close syntax: hl order market close <coin>")
676
+ if args.stake is not None:
677
+ _exit_with_error("--stake cannot be used with market close")
678
+ if args.leverage is not None or args.cross or args.isolated:
679
+ _exit_with_error("--leverage/--cross/--isolated cannot be used with market close")
680
+ await _call(legacy.order_market_close, ctx, args.a, slippage=args.slippage, ratio=args.ratio)
681
+ else:
682
+ if args.ratio != 1.0:
683
+ _exit_with_error("--ratio is only supported with: hl order market close <coin>")
684
+ size, coin = _parse_market_shape(args)
685
+ await _call(
686
+ legacy.order_market,
687
+ ctx,
688
+ args.side,
689
+ size,
690
+ coin,
691
+ reduce_only=args.reduce_only,
692
+ slippage=args.slippage,
693
+ stake=args.stake,
694
+ leverage=args.leverage,
695
+ cross=args.cross,
696
+ isolated=args.isolated,
697
+ )
698
+ elif sc == "twap":
699
+ await _call(
700
+ legacy.order_twap,
701
+ ctx,
702
+ args.side,
703
+ args.size,
704
+ args.coin,
705
+ args.interval,
706
+ stake=args.stake,
707
+ reduce_only=args.reduce_only,
708
+ randomize=args.randomize,
709
+ leverage=args.leverage,
710
+ cross=args.cross,
711
+ isolated=args.isolated,
712
+ )
713
+ elif sc == "tpsl":
714
+ await _call(
715
+ legacy.order_tpsl,
716
+ ctx,
717
+ args.coin,
718
+ tp=args.tp,
719
+ sl=args.sl,
720
+ ratio=args.ratio,
721
+ )
722
+ elif sc == "twap-cancel":
723
+ await _call(legacy.order_twap_cancel, ctx, args.coin, args.twap_id)
724
+ elif sc == "cancel":
725
+ await _call(legacy.order_cancel, ctx, oid=args.oid)
726
+ elif sc == "cancel-all":
727
+ await _call(legacy.order_cancel_all, ctx, yes=args.yes, coin=args.coin)
728
+ elif sc == "set-leverage":
729
+ await _call(
730
+ legacy.order_set_leverage,
731
+ ctx,
732
+ args.coin,
733
+ args.leverage,
734
+ cross=args.cross,
735
+ isolated=args.isolated,
736
+ )
737
+ elif sc == "configure":
738
+ await _call(legacy.order_configure, ctx, slippage=args.slippage)
739
+ else:
740
+ _exit_with_error(f"Unknown order subcommand: {sc}")
741
+ return
742
+
743
+ if cmd == "asset":
744
+ sc = args.asset_command
745
+ if sc is None:
746
+ _exit_with_error("Missing asset subcommand. Run: hl asset -h")
747
+ if sc == "price":
748
+ await _call(legacy.asset_price, ctx, args.coin, watch=args.watch)
749
+ elif sc == "book":
750
+ await _call(legacy.asset_book, ctx, args.coin, watch=args.watch)
751
+ elif sc == "leverage":
752
+ await _call(legacy.asset_leverage, ctx, args.coin, user=args.user, watch=args.watch)
753
+ else:
754
+ _exit_with_error(f"Unknown asset subcommand: {sc}")
755
+ return
756
+
757
+ if cmd == "markets":
758
+ sc = args.markets_command
759
+ if sc is None:
760
+ _exit_with_error("Missing markets subcommand. Run: hl markets -h")
761
+ if sc == "ls":
762
+ await _call(
763
+ legacy.markets_ls,
764
+ ctx,
765
+ spot_only=args.spot_only,
766
+ perp_only=args.perp_only,
767
+ category=args.category,
768
+ sort_by=args.sort_by,
769
+ watch=args.watch,
770
+ )
771
+ elif sc == "search":
772
+ await _call(
773
+ legacy.markets_search,
774
+ ctx,
775
+ args.query,
776
+ spot_only=args.spot_only,
777
+ perp_only=args.perp_only,
778
+ category=args.category,
779
+ sort_by=args.sort_by,
780
+ )
781
+ else:
782
+ _exit_with_error(f"Unknown markets subcommand: {sc}")
783
+ return
784
+
785
+ if cmd == "referral":
786
+ sc = args.referral_command
787
+ if sc is None:
788
+ _exit_with_error("Missing referral subcommand. Run: hl referral -h")
789
+ if sc == "set":
790
+ await _call(legacy.referral_set, ctx, args.code)
791
+ elif sc == "status":
792
+ await _call(legacy.referral_status, ctx)
793
+ else:
794
+ _exit_with_error(f"Unknown referral subcommand: {sc}")
795
+ return
796
+
797
+ if cmd == "completion":
798
+ sc = args.completion_command
799
+ if sc is None:
800
+ _exit_with_error("Missing completion subcommand. Run: hl completion -h")
801
+ _print_completion(sc)
802
+ return
803
+
804
+ _exit_with_error(f"Unknown command: {cmd}")
805
+
806
+
807
+ def main(argv: list[str] | None = None) -> None:
808
+ parser = _build_parser()
809
+ args = parser.parse_args(argv)
810
+ asyncio.run(dispatch(args, parser))
811
+
812
+
813
+ if __name__ == "__main__":
814
+ main()