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/__init__.py +1 -0
- hl_cli/cli/__init__.py +1 -0
- hl_cli/cli/argparse_main.py +814 -0
- hl_cli/cli/markets_tui.py +399 -0
- hl_cli/cli/runtime.py +82 -0
- hl_cli/commands/__init__.py +1 -0
- hl_cli/commands/app.py +1081 -0
- hl_cli/commands/order.py +918 -0
- hl_cli/core/__init__.py +1 -0
- hl_cli/core/context.py +156 -0
- hl_cli/core/order_config.py +23 -0
- hl_cli/infra/__init__.py +1 -0
- hl_cli/infra/db.py +277 -0
- hl_cli/infra/paths.py +5 -0
- hl_cli/utils/__init__.py +1 -0
- hl_cli/utils/market_table.py +66 -0
- hl_cli/utils/output.py +476 -0
- hl_cli/utils/validators.py +45 -0
- hl_cli/utils/watch.py +28 -0
- hyperliquid_cli_python-0.1.0.dist-info/METADATA +269 -0
- hyperliquid_cli_python-0.1.0.dist-info/RECORD +25 -0
- hyperliquid_cli_python-0.1.0.dist-info/WHEEL +5 -0
- hyperliquid_cli_python-0.1.0.dist-info/entry_points.txt +2 -0
- hyperliquid_cli_python-0.1.0.dist-info/licenses/LICENSE +32 -0
- hyperliquid_cli_python-0.1.0.dist-info/top_level.txt +1 -0
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
|