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
|
@@ -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()
|