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.
hl_cli/utils/output.py ADDED
@@ -0,0 +1,476 @@
1
+ import json
2
+ from typing import Any, Iterable
3
+
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from .market_table import (
8
+ build_market_table,
9
+ market_table_columns,
10
+ market_table_row_values,
11
+ market_table_widths,
12
+ )
13
+
14
+ console = Console()
15
+
16
+
17
+ def out(data: Any, as_json: bool = False) -> None:
18
+ if as_json:
19
+ print(json.dumps(data, ensure_ascii=False, indent=2))
20
+ return
21
+ if _render_known(data):
22
+ return
23
+ if isinstance(data, str):
24
+ console.print(data)
25
+ return
26
+ console.print_json(json.dumps(data, ensure_ascii=False))
27
+
28
+
29
+ def _render_known(data: Any) -> bool:
30
+ if isinstance(data, list):
31
+ if _print_open_orders_list(data):
32
+ return True
33
+ if _print_accounts_list(data):
34
+ return True
35
+ return False
36
+
37
+ if not isinstance(data, dict):
38
+ return False
39
+
40
+ if "perpMarkets" in data and "spotMarkets" in data:
41
+ _print_markets_payload(data)
42
+ return True
43
+
44
+ if (
45
+ "positions" in data
46
+ and "spotBalances" in data
47
+ and "accountValue" in data
48
+ and "totalMarginUsed" in data
49
+ ):
50
+ _print_portfolio_payload(data)
51
+ return True
52
+
53
+ if "positions" in data and isinstance(data.get("positions"), list):
54
+ _print_positions_payload(data)
55
+ return True
56
+
57
+ if _print_account_record(data):
58
+ return True
59
+
60
+ if "spotBalances" in data and "perpBalance" in data:
61
+ _print_balances_payload(data)
62
+ return True
63
+
64
+ if "coin" in data and "price" in data and len(data.keys()) <= 3:
65
+ console.print("Market Price")
66
+ console.print(f"- Asset: {data.get('coin')}")
67
+ console.print(f"- Price: {_fmt_price(data.get('price'))}")
68
+ return True
69
+
70
+ if "coin" in data and "markPx" in data and "maxLeverage" in data and "margin" in data:
71
+ _print_asset_leverage_payload(data)
72
+ return True
73
+
74
+ if "levels" in data and isinstance(data.get("levels"), list):
75
+ _print_book_payload(data)
76
+ return True
77
+
78
+ if "slippage" in data and len(data.keys()) == 1:
79
+ console.print("Order Defaults")
80
+ console.print(f"- Slippage: {_fmt_pct(data.get('slippage'))}")
81
+ return True
82
+
83
+ if "twapCancel" in data and isinstance(data["twapCancel"], dict):
84
+ _print_twap_cancel_payload(data["twapCancel"])
85
+ return True
86
+
87
+ if data.get("status") == "ok" and isinstance(data.get("response"), dict):
88
+ _print_exchange_response(data["response"])
89
+ return True
90
+
91
+ if _print_cancel_noop(data):
92
+ return True
93
+
94
+ if data.get("status") == "err":
95
+ console.print("[red]❌ Request failed[/red]")
96
+ console.print(f"Reason: {data.get('response')}")
97
+ return True
98
+
99
+ if _print_flat_dict(data):
100
+ return True
101
+
102
+ return False
103
+
104
+
105
+ def _print_positions_payload(data: dict[str, Any]) -> None:
106
+ positions = data.get("positions", [])
107
+ if positions:
108
+ tbl = Table(title="Positions")
109
+ for c in ["coin", "size", "entryPx", "positionValue", "unrealizedPnl", "leverage", "liquidationPx"]:
110
+ tbl.add_column(c)
111
+ for p in positions:
112
+ tbl.add_row(
113
+ str(p.get("coin", "")),
114
+ str(p.get("size", "")),
115
+ str(p.get("entryPx", "")),
116
+ str(p.get("positionValue", "")),
117
+ str(p.get("unrealizedPnl", "")),
118
+ str(p.get("leverage", "")),
119
+ str(p.get("liquidationPx", "")),
120
+ )
121
+ console.print(tbl)
122
+ else:
123
+ console.print("No open positions")
124
+
125
+ ms = data.get("marginSummary")
126
+ if isinstance(ms, dict):
127
+ console.print("Margin Summary")
128
+ console.print(f"- Account value: {_fmt_usd(ms.get('accountValue'))}")
129
+ console.print(f"- Total margin used: {_fmt_usd(ms.get('totalMarginUsed'))}")
130
+
131
+
132
+ def _print_balances_payload(data: dict[str, Any]) -> None:
133
+ console.print("Balances")
134
+ console.print(f"- Perp balance: {_fmt_usd(data.get('perpBalance'))}")
135
+ balances = data.get("spotBalances", [])
136
+ if not balances:
137
+ console.print("No spot balances")
138
+ return
139
+
140
+ tbl = Table(title="Spot Balances")
141
+ cols = ["token", "total", "hold", "available"]
142
+ for c in cols:
143
+ tbl.add_column(c)
144
+ for b in balances:
145
+ tbl.add_row(*[str(b.get(c, "")) for c in cols])
146
+ console.print(tbl)
147
+
148
+
149
+ def _iter_statuses(statuses: Iterable[Any]) -> None:
150
+ for s in statuses:
151
+ if isinstance(s, str):
152
+ console.print(s)
153
+ continue
154
+ if not isinstance(s, dict):
155
+ console.print(str(s))
156
+ continue
157
+ if "error" in s:
158
+ console.print("[red]❌ Order rejected[/red]")
159
+ console.print(f"Reason: {s['error']}")
160
+ continue
161
+ if "filled" in s and isinstance(s["filled"], dict):
162
+ f = s["filled"]
163
+ console.print("[green]✅ Order filled[/green]")
164
+ console.print(f"Filled size: {f.get('totalSz')}")
165
+ console.print(f"Average price: {_fmt_usd(f.get('avgPx'))}")
166
+ console.print(f"Order ID: {f.get('oid')}")
167
+ continue
168
+ if "resting" in s and isinstance(s["resting"], dict):
169
+ r = s["resting"]
170
+ console.print("[cyan]🕒 Order resting on book[/cyan]")
171
+ console.print(f"Order ID: {r.get('oid')}")
172
+ continue
173
+ console.print(json.dumps(s, ensure_ascii=False))
174
+
175
+
176
+ def _print_exchange_response(resp: dict[str, Any]) -> None:
177
+ rtype = resp.get("type")
178
+ data = resp.get("data")
179
+
180
+ if rtype == "order":
181
+ statuses = []
182
+ if isinstance(data, dict):
183
+ statuses = data.get("statuses") or []
184
+ if statuses:
185
+ _iter_statuses(statuses)
186
+ return
187
+ console.print("Order request accepted")
188
+ return
189
+
190
+ if rtype in {"cancel", "batchModify", "twapOrder", "twapCancel", "default"}:
191
+ if isinstance(data, dict) and "statuses" in data:
192
+ _iter_statuses(data.get("statuses") or [])
193
+ return
194
+ if isinstance(data, dict) and "status" in data:
195
+ s = data.get("status")
196
+ if isinstance(s, dict) and "error" in s:
197
+ console.print(f"[red]Error:[/red] {s['error']}")
198
+ else:
199
+ console.print(f"{rtype}: {s}")
200
+ return
201
+ console.print(f"{rtype}: ok")
202
+ return
203
+
204
+ console.print_json(json.dumps({"status": "ok", "response": resp}, ensure_ascii=False))
205
+
206
+
207
+ def _print_open_orders_list(data: list[Any]) -> bool:
208
+ if not all(isinstance(x, dict) for x in data):
209
+ return False
210
+ rows = [x for x in data if {"oid", "coin", "side", "sz", "limitPx"}.issubset(x.keys())]
211
+ if len(rows) != len(data):
212
+ return False
213
+ if not rows:
214
+ console.print("No open orders")
215
+ return True
216
+ tbl = Table(title="Open Orders")
217
+ for c in ["oid", "coin", "side", "sz", "limitPx", "timestamp"]:
218
+ tbl.add_column(c)
219
+ for r in rows:
220
+ tbl.add_row(
221
+ str(r.get("oid", "")),
222
+ str(r.get("coin", "")),
223
+ str(r.get("side", "")),
224
+ str(r.get("sz", "")),
225
+ _fmt_usd(r.get("limitPx")),
226
+ str(r.get("timestamp", "")),
227
+ )
228
+ console.print(tbl)
229
+ return True
230
+
231
+
232
+ def _print_accounts_list(data: list[Any]) -> bool:
233
+ if not all(isinstance(x, dict) for x in data):
234
+ return False
235
+ rows = [x for x in data if {"alias", "user_address", "type", "is_default"}.issubset(x.keys())]
236
+ if len(rows) != len(data):
237
+ return False
238
+ tbl = Table(title="Accounts")
239
+ for c in ["alias", "user_address", "type", "source", "api_wallet_public_key", "is_default"]:
240
+ tbl.add_column(c)
241
+ for r in rows:
242
+ tbl.add_row(
243
+ str(r.get("alias", "")),
244
+ str(r.get("user_address", "")),
245
+ str(r.get("type", "")),
246
+ str(r.get("source", "")),
247
+ str(r.get("api_wallet_public_key") or "-"),
248
+ "yes" if r.get("is_default") else "",
249
+ )
250
+ console.print(tbl)
251
+ return True
252
+
253
+
254
+ def _print_account_record(data: dict[str, Any]) -> bool:
255
+ if not {"alias", "user_address", "type"}.issubset(data.keys()):
256
+ return False
257
+ console.print("[green]✅ Account saved[/green]")
258
+ console.print(f"Alias: {data.get('alias')}")
259
+ console.print(f"Address: {data.get('user_address')}")
260
+ console.print(f"Type: {data.get('type')}")
261
+ if data.get("api_wallet_public_key"):
262
+ console.print(f"API wallet: {data.get('api_wallet_public_key')}")
263
+ return True
264
+
265
+
266
+ def _print_portfolio_payload(data: dict[str, Any]) -> None:
267
+ console.print("Portfolio")
268
+ console.print(f"- Account value: {_fmt_usd(data.get('accountValue'))}")
269
+ console.print(f"- Margin used: {_fmt_usd(data.get('totalMarginUsed'))}")
270
+ _print_positions_payload({"positions": data.get("positions", [])})
271
+ _print_balances_payload({"spotBalances": data.get("spotBalances", []), "perpBalance": data.get("accountValue")})
272
+
273
+
274
+ def _print_markets_payload(data: dict[str, Any]) -> None:
275
+ perp = data.get("perpMarkets", [])
276
+ spot = data.get("spotMarkets", [])
277
+ show_perp_category = any("category" in r for r in perp)
278
+ show_spot_category = any("category" in r for r in spot)
279
+ console.print(f"Markets: {len(perp)} perp / {len(spot)} spot")
280
+ if perp:
281
+ columns = market_table_columns(
282
+ include_category=show_perp_category,
283
+ show_perp_only_fields=True,
284
+ )
285
+ rendered_rows = [
286
+ market_table_row_values(
287
+ r,
288
+ include_category=show_perp_category,
289
+ show_perp_only_fields=True,
290
+ format_price=_fmt_price,
291
+ format_usd=_fmt_usd,
292
+ format_rate_pct=_fmt_rate_pct,
293
+ )
294
+ for r in perp
295
+ ]
296
+ tbl = build_market_table(
297
+ title="Perp Markets",
298
+ columns=columns,
299
+ rendered_rows=rendered_rows,
300
+ widths=market_table_widths(columns, rendered_rows),
301
+ )
302
+ console.print(tbl)
303
+ if spot:
304
+ columns = market_table_columns(
305
+ include_category=show_spot_category,
306
+ show_perp_only_fields=False,
307
+ )
308
+ rendered_rows = [
309
+ market_table_row_values(
310
+ r,
311
+ include_category=show_spot_category,
312
+ show_perp_only_fields=False,
313
+ format_price=_fmt_price,
314
+ format_usd=_fmt_usd,
315
+ format_rate_pct=_fmt_rate_pct,
316
+ )
317
+ for r in spot
318
+ ]
319
+ tbl = build_market_table(
320
+ title="Spot Markets",
321
+ columns=columns,
322
+ rendered_rows=rendered_rows,
323
+ widths=market_table_widths(columns, rendered_rows),
324
+ )
325
+ console.print(tbl)
326
+
327
+
328
+ def _print_asset_leverage_payload(data: dict[str, Any]) -> None:
329
+ console.print("Asset Leverage")
330
+ console.print(f"- Asset: {data.get('coin')}")
331
+ console.print(f"- Mark price: {_fmt_usd(data.get('markPx'))}")
332
+ console.print(f"- Max leverage: {data.get('maxLeverage')}x")
333
+ margin = data.get("margin") or {}
334
+ console.print("Margin")
335
+ console.print(f"- Account value: {_fmt_usd(margin.get('accountValue'))}")
336
+ console.print(f"- Margin used: {_fmt_usd(margin.get('totalMarginUsed'))}")
337
+ console.print(f"- Available margin: {_fmt_usd(margin.get('availableMargin'))}")
338
+ pos = data.get("position")
339
+ if isinstance(pos, dict):
340
+ console.print("Position")
341
+ console.print(f"- Size: {pos.get('szi')}")
342
+ console.print(f"- Entry: {_fmt_usd(pos.get('entryPx'))}")
343
+ console.print(f"- Value: {_fmt_usd(pos.get('positionValue'))}")
344
+ console.print(f"- Unrealized PnL: {_fmt_usd(pos.get('unrealizedPnl'))}")
345
+ else:
346
+ console.print("Position: none")
347
+
348
+
349
+ def _print_book_payload(data: dict[str, Any]) -> None:
350
+ levels = data.get("levels", [[], []])
351
+ bids = levels[0][:10] if len(levels) > 0 else []
352
+ asks = levels[1][:10] if len(levels) > 1 else []
353
+ if asks:
354
+ tbl = Table(title=f"Asks ({data.get('coin', '-')})")
355
+ for c in ["px", "sz", "n"]:
356
+ tbl.add_column(c)
357
+ for x in asks[::-1]:
358
+ tbl.add_row(_fmt_usd(x.get("px")), str(x.get("sz", "")), str(x.get("n", "")))
359
+ console.print(tbl)
360
+ if bids:
361
+ tbl = Table(title=f"Bids ({data.get('coin', '-')})")
362
+ for c in ["px", "sz", "n"]:
363
+ tbl.add_column(c)
364
+ for x in bids:
365
+ tbl.add_row(_fmt_usd(x.get("px")), str(x.get("sz", "")), str(x.get("n", "")))
366
+ console.print(tbl)
367
+
368
+
369
+ def _print_twap_cancel_payload(data: dict[str, Any]) -> None:
370
+ coin = data.get("coin")
371
+ twap_id = data.get("twapId")
372
+ response = data.get("response") or {}
373
+ status = response.get("response", {}).get("data", {}).get("status", {})
374
+ if isinstance(status, dict) and status.get("error"):
375
+ console.print("[red]❌ TWAP cancel rejected[/red]")
376
+ console.print(f"Asset: {coin}")
377
+ console.print(f"TWAP ID: {twap_id}")
378
+ console.print(f"Reason: {status.get('error')}")
379
+ return
380
+ console.print("[green]✅ TWAP cancel submitted[/green]")
381
+ console.print(f"Asset: {coin}")
382
+ console.print(f"TWAP ID: {twap_id}")
383
+
384
+
385
+ def _print_cancel_noop(data: dict[str, Any]) -> bool:
386
+ if "cancelled" in data and "reason" in data:
387
+ console.print(data.get("message", "No-op"))
388
+ return True
389
+ return False
390
+
391
+
392
+ def _print_flat_dict(data: dict[str, Any]) -> bool:
393
+ if not data:
394
+ return False
395
+ if any(isinstance(v, (dict, list, tuple, set)) for v in data.values()):
396
+ return False
397
+ for k, v in data.items():
398
+ console.print(f"{k}: {v}")
399
+ return True
400
+
401
+
402
+ def _fmt_usd(value: Any) -> str:
403
+ if value is None:
404
+ return "-"
405
+ try:
406
+ n = float(value)
407
+ except (TypeError, ValueError):
408
+ return str(value)
409
+ return f"${n:,.2f}"
410
+
411
+
412
+ def _fmt_price(value: Any) -> str:
413
+ if value is None:
414
+ return "-"
415
+ try:
416
+ n = float(value)
417
+ except (TypeError, ValueError):
418
+ return str(value)
419
+
420
+ abs_n = abs(n)
421
+ if abs_n >= 1000:
422
+ s = f"{n:,.2f}"
423
+ elif abs_n >= 1:
424
+ s = f"{n:,.4f}"
425
+ elif abs_n >= 0.01:
426
+ s = f"{n:,.4f}"
427
+ elif abs_n >= 0.0001:
428
+ s = f"{n:,.6f}"
429
+ else:
430
+ s = f"{n:,.8f}"
431
+
432
+ if "." in s:
433
+ s = s.rstrip("0").rstrip(".")
434
+ return f"${s}"
435
+
436
+
437
+ def _fmt_pct(value: Any) -> str:
438
+ if value is None:
439
+ return "-"
440
+ try:
441
+ n = float(value)
442
+ except (TypeError, ValueError):
443
+ return str(value)
444
+ return f"{n:+.2f}%"
445
+
446
+
447
+ def _fmt_rate_pct(value: Any) -> str:
448
+ if value is None:
449
+ return "-"
450
+ try:
451
+ n = float(value)
452
+ except (TypeError, ValueError):
453
+ return str(value)
454
+
455
+ abs_n = abs(n)
456
+ if abs_n >= 1:
457
+ s = f"{n:+.2f}"
458
+ elif abs_n >= 0.01:
459
+ s = f"{n:+.4f}"
460
+ else:
461
+ s = f"{n:+.6f}"
462
+
463
+ if "." in s:
464
+ sign = s[0] if s[0] in "+-" else ""
465
+ digits = s[1:] if sign else s
466
+ digits = digits.rstrip("0").rstrip(".")
467
+ s = f"{sign}{digits}"
468
+ return f"{s}%"
469
+
470
+
471
+ def out_error(message: str) -> None:
472
+ console.print(f"[red]Error:[/red] {message}")
473
+
474
+
475
+ def out_success(message: str) -> None:
476
+ console.print(f"[green]{message}[/green]")
@@ -0,0 +1,45 @@
1
+ import re
2
+
3
+
4
+ def validate_address(value: str) -> str:
5
+ if not re.fullmatch(r"0x[a-fA-F0-9]{40}", value):
6
+ raise ValueError(f"Invalid address: {value}")
7
+ return value
8
+
9
+
10
+ def normalize_private_key(value: str) -> str:
11
+ key = value if value.startswith("0x") else f"0x{value}"
12
+ if not re.fullmatch(r"0x[a-fA-F0-9]{64}", key):
13
+ raise ValueError("Invalid private key format")
14
+ return key
15
+
16
+
17
+ def validate_positive_number(value: str, name: str) -> float:
18
+ num = float(value)
19
+ if num <= 0:
20
+ raise ValueError(f"{name} must be a positive number")
21
+ return num
22
+
23
+
24
+ def validate_positive_integer(value: str, name: str) -> int:
25
+ num = int(value)
26
+ if num <= 0:
27
+ raise ValueError(f"{name} must be a positive integer")
28
+ return num
29
+
30
+
31
+ def normalize_side(value: str) -> str:
32
+ lower = value.lower()
33
+ if lower in {"buy", "long"}:
34
+ return "buy"
35
+ if lower in {"sell", "short"}:
36
+ return "sell"
37
+ raise ValueError('Side must be "buy", "sell", "long", or "short"')
38
+
39
+
40
+ def normalize_tif(value: str) -> str:
41
+ mapping = {"gtc": "Gtc", "ioc": "Ioc", "alo": "Alo"}
42
+ try:
43
+ return mapping[value.lower()]
44
+ except KeyError as exc:
45
+ raise ValueError('Time-in-force must be "Gtc", "Ioc", or "Alo"') from exc
hl_cli/utils/watch.py ADDED
@@ -0,0 +1,28 @@
1
+ import json
2
+ import time
3
+ from typing import Callable, TypeVar
4
+
5
+ from rich.console import Console
6
+
7
+ console = Console()
8
+ T = TypeVar("T")
9
+
10
+
11
+ def watch_loop(
12
+ fetcher: Callable[[], T],
13
+ renderer: Callable[[T], None],
14
+ *,
15
+ as_json: bool,
16
+ interval: float = 1.0,
17
+ ) -> None:
18
+ try:
19
+ while True:
20
+ data = fetcher()
21
+ if as_json:
22
+ print(json.dumps(data, ensure_ascii=False))
23
+ else:
24
+ console.clear()
25
+ renderer(data)
26
+ time.sleep(interval)
27
+ except KeyboardInterrupt:
28
+ return