hyperliquid-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hyperliquid_cli-0.1.0/.github/workflows/publish.yml +29 -0
- hyperliquid_cli-0.1.0/.gitignore +7 -0
- hyperliquid_cli-0.1.0/LICENSE +21 -0
- hyperliquid_cli-0.1.0/PKG-INFO +79 -0
- hyperliquid_cli-0.1.0/README.md +53 -0
- hyperliquid_cli-0.1.0/pyproject.toml +41 -0
- hyperliquid_cli-0.1.0/src/hl_cli/__init__.py +0 -0
- hyperliquid_cli-0.1.0/src/hl_cli/client.py +68 -0
- hyperliquid_cli-0.1.0/src/hl_cli/commands/__init__.py +0 -0
- hyperliquid_cli-0.1.0/src/hl_cli/commands/act.py +277 -0
- hyperliquid_cli-0.1.0/src/hl_cli/commands/orient.py +174 -0
- hyperliquid_cli-0.1.0/src/hl_cli/commands/research.py +287 -0
- hyperliquid_cli-0.1.0/src/hl_cli/commands/review.py +84 -0
- hyperliquid_cli-0.1.0/src/hl_cli/main.py +30 -0
- hyperliquid_cli-0.1.0/src/hl_cli/output.py +84 -0
- hyperliquid_cli-0.1.0/src/hl_cli/resolver.py +151 -0
- hyperliquid_cli-0.1.0/tests/__init__.py +0 -0
- hyperliquid_cli-0.1.0/tests/__snapshots__/test_act.ambr +57 -0
- hyperliquid_cli-0.1.0/tests/__snapshots__/test_orient.ambr +117 -0
- hyperliquid_cli-0.1.0/tests/__snapshots__/test_research.ambr +168 -0
- hyperliquid_cli-0.1.0/tests/__snapshots__/test_review.ambr +49 -0
- hyperliquid_cli-0.1.0/tests/conftest.py +256 -0
- hyperliquid_cli-0.1.0/tests/test_act.py +111 -0
- hyperliquid_cli-0.1.0/tests/test_orient.py +88 -0
- hyperliquid_cli-0.1.0/tests/test_research.py +107 -0
- hyperliquid_cli-0.1.0/tests/test_review.py +70 -0
- hyperliquid_cli-0.1.0/uv.lock +1244 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v4
|
|
14
|
+
- run: uv run --extra test pytest -v
|
|
15
|
+
|
|
16
|
+
publish:
|
|
17
|
+
needs: test
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
environment: pypi
|
|
20
|
+
permissions:
|
|
21
|
+
id-token: write
|
|
22
|
+
contents: read
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: astral-sh/setup-uv@v4
|
|
26
|
+
- run: uv build
|
|
27
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
28
|
+
with:
|
|
29
|
+
packages-dir: dist/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tab55
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hyperliquid-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Agent-first CLI for Hyperliquid trading
|
|
5
|
+
Author: tab55
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: agent,cli,defi,hyperliquid,trading
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: hyperliquid-python-sdk>=0.22.0
|
|
21
|
+
Requires-Dist: typer>=0.9.0
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'test'
|
|
24
|
+
Requires-Dist: syrupy>=4.0; extra == 'test'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# hyperliquid-cli
|
|
28
|
+
|
|
29
|
+
Agent-first CLI for Hyperliquid perpetual futures trading. Thin Python wrapper over [hyperliquid-python-sdk](https://github.com/hyperliquid-x/hyperliquid-python-sdk).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv tool install hyperliquid-cli
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Read commands (no auth needed)
|
|
41
|
+
hl prices BTC ETH
|
|
42
|
+
hl book BTC --depth 10
|
|
43
|
+
hl assets TSLA
|
|
44
|
+
hl funding BTC --history
|
|
45
|
+
hl candles BTC 1h --limit 50
|
|
46
|
+
|
|
47
|
+
# Account commands (needs HL_PRIVATE_KEY or --address)
|
|
48
|
+
hl status
|
|
49
|
+
hl balance
|
|
50
|
+
hl positions
|
|
51
|
+
hl orders
|
|
52
|
+
hl fills
|
|
53
|
+
|
|
54
|
+
# Write commands (needs HL_PRIVATE_KEY)
|
|
55
|
+
hl order BTC buy 0.1 --market
|
|
56
|
+
hl order BTC buy 0.1 95000 --tp 100000 --sl 90000
|
|
57
|
+
hl cancel 77738308
|
|
58
|
+
hl cancel-all BTC
|
|
59
|
+
hl leverage BTC 10
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Auth
|
|
63
|
+
|
|
64
|
+
Set `HL_PRIVATE_KEY` env var with your Hyperliquid API wallet key (can trade but cannot withdraw).
|
|
65
|
+
|
|
66
|
+
Read-only commands work without auth using `--address`.
|
|
67
|
+
|
|
68
|
+
## Design
|
|
69
|
+
|
|
70
|
+
- JSON-only output (stdout), debug/warnings to stderr
|
|
71
|
+
- Rich exit codes (0=success, 1=input, 2=auth, 3=API, 4=asset, 5=rejected)
|
|
72
|
+
- Structured errors with recovery hints
|
|
73
|
+
- Field projection via `--fields`
|
|
74
|
+
- HIP-3 asset auto-resolution (e.g. `TSLA` → `xyz:TSLA`)
|
|
75
|
+
- Trade logging to `data/trades.jsonl`
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# hyperliquid-cli
|
|
2
|
+
|
|
3
|
+
Agent-first CLI for Hyperliquid perpetual futures trading. Thin Python wrapper over [hyperliquid-python-sdk](https://github.com/hyperliquid-x/hyperliquid-python-sdk).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
uv tool install hyperliquid-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Read commands (no auth needed)
|
|
15
|
+
hl prices BTC ETH
|
|
16
|
+
hl book BTC --depth 10
|
|
17
|
+
hl assets TSLA
|
|
18
|
+
hl funding BTC --history
|
|
19
|
+
hl candles BTC 1h --limit 50
|
|
20
|
+
|
|
21
|
+
# Account commands (needs HL_PRIVATE_KEY or --address)
|
|
22
|
+
hl status
|
|
23
|
+
hl balance
|
|
24
|
+
hl positions
|
|
25
|
+
hl orders
|
|
26
|
+
hl fills
|
|
27
|
+
|
|
28
|
+
# Write commands (needs HL_PRIVATE_KEY)
|
|
29
|
+
hl order BTC buy 0.1 --market
|
|
30
|
+
hl order BTC buy 0.1 95000 --tp 100000 --sl 90000
|
|
31
|
+
hl cancel 77738308
|
|
32
|
+
hl cancel-all BTC
|
|
33
|
+
hl leverage BTC 10
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Auth
|
|
37
|
+
|
|
38
|
+
Set `HL_PRIVATE_KEY` env var with your Hyperliquid API wallet key (can trade but cannot withdraw).
|
|
39
|
+
|
|
40
|
+
Read-only commands work without auth using `--address`.
|
|
41
|
+
|
|
42
|
+
## Design
|
|
43
|
+
|
|
44
|
+
- JSON-only output (stdout), debug/warnings to stderr
|
|
45
|
+
- Rich exit codes (0=success, 1=input, 2=auth, 3=API, 4=asset, 5=rejected)
|
|
46
|
+
- Structured errors with recovery hints
|
|
47
|
+
- Field projection via `--fields`
|
|
48
|
+
- HIP-3 asset auto-resolution (e.g. `TSLA` → `xyz:TSLA`)
|
|
49
|
+
- Trade logging to `data/trades.jsonl`
|
|
50
|
+
|
|
51
|
+
## License
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hyperliquid-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Agent-first CLI for Hyperliquid trading"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{ name = "tab55" }]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["hyperliquid", "trading", "cli", "agent", "defi"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer>=0.9.0",
|
|
24
|
+
"hyperliquid-python-sdk>=0.22.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = ["pytest>=8.0", "syrupy>=4.0"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
hl = "hl_cli.main:app"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/hl_cli"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
File without changes
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""SDK client factory — creates Info and Exchange instances."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import eth_account
|
|
6
|
+
from hyperliquid.exchange import Exchange
|
|
7
|
+
from hyperliquid.info import Info
|
|
8
|
+
from hyperliquid.utils.constants import MAINNET_API_URL
|
|
9
|
+
|
|
10
|
+
from hl_cli.output import die
|
|
11
|
+
|
|
12
|
+
TESTNET_API_URL = "https://api.hyperliquid-testnet.xyz"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _base_url(testnet: bool) -> str:
|
|
16
|
+
return TESTNET_API_URL if testnet else MAINNET_API_URL
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _get_key():
|
|
20
|
+
key = os.environ.get("HL_PRIVATE_KEY")
|
|
21
|
+
if not key:
|
|
22
|
+
die(
|
|
23
|
+
"auth_error",
|
|
24
|
+
"HL_PRIVATE_KEY environment variable not set",
|
|
25
|
+
hint="Set HL_PRIVATE_KEY to your API wallet private key",
|
|
26
|
+
exit_code=2,
|
|
27
|
+
)
|
|
28
|
+
return key
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_address(explicit: str = None) -> str:
|
|
32
|
+
"""Get wallet address from explicit arg or HL_PRIVATE_KEY."""
|
|
33
|
+
if explicit:
|
|
34
|
+
return explicit
|
|
35
|
+
key = os.environ.get("HL_PRIVATE_KEY")
|
|
36
|
+
if key:
|
|
37
|
+
return eth_account.Account.from_key(key).address
|
|
38
|
+
die(
|
|
39
|
+
"auth_error",
|
|
40
|
+
"No address available",
|
|
41
|
+
hint="Set HL_PRIVATE_KEY or use --address",
|
|
42
|
+
exit_code=2,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_info(testnet: bool = False) -> Info:
|
|
47
|
+
"""Create Info client (default perps only, dexes loaded lazily by resolver)."""
|
|
48
|
+
return Info(_base_url(testnet), skip_ws=True)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_exchange(testnet: bool = False) -> Exchange:
|
|
52
|
+
"""Create Exchange client (requires HL_PRIVATE_KEY)."""
|
|
53
|
+
key = _get_key()
|
|
54
|
+
account = eth_account.Account.from_key(key)
|
|
55
|
+
return Exchange(account, _base_url(testnet), account_address=account.address)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_all_mids(info: Info) -> dict:
|
|
59
|
+
"""Get all mid prices including HIP-3 deployer assets."""
|
|
60
|
+
mids = info.all_mids()
|
|
61
|
+
try:
|
|
62
|
+
dex_list = info.post("/info", {"type": "perpDexs"})
|
|
63
|
+
for dex_info in dex_list[1:]:
|
|
64
|
+
dex_mids = info.post("/info", {"type": "allMids", "dex": dex_info["name"]})
|
|
65
|
+
mids.update(dex_mids)
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
return mids
|
|
File without changes
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Act commands — order, cancel, cancel-all, leverage."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from hl_cli.client import get_address, get_exchange
|
|
12
|
+
from hl_cli.main import app
|
|
13
|
+
from hl_cli.output import die, format_timestamp, output
|
|
14
|
+
from hl_cli.resolver import get_resolver
|
|
15
|
+
|
|
16
|
+
SIDE_MAP = {"buy": True, "long": True, "sell": False, "short": False}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _parse_order_response(resp, asset: str, side_str: str, size: str):
|
|
20
|
+
"""Parse SDK order response into CLI output format."""
|
|
21
|
+
if resp.get("status") == "err":
|
|
22
|
+
die("order_rejected", resp.get("response", str(resp)), exit_code=5)
|
|
23
|
+
|
|
24
|
+
data = resp.get("response", {}).get("data", {})
|
|
25
|
+
statuses = data.get("statuses", [])
|
|
26
|
+
if not statuses:
|
|
27
|
+
die("order_rejected", "No status in response", exit_code=5)
|
|
28
|
+
|
|
29
|
+
st = statuses[0]
|
|
30
|
+
if "error" in st:
|
|
31
|
+
die("order_rejected", st["error"], exit_code=5)
|
|
32
|
+
|
|
33
|
+
if "filled" in st:
|
|
34
|
+
filled = st["filled"]
|
|
35
|
+
return {
|
|
36
|
+
"status": "filled",
|
|
37
|
+
"oid": filled.get("oid", 0),
|
|
38
|
+
"asset": asset,
|
|
39
|
+
"side": side_str,
|
|
40
|
+
"size": filled.get("totalSz", size),
|
|
41
|
+
"avg_price": filled.get("avgPx", ""),
|
|
42
|
+
}
|
|
43
|
+
elif "resting" in st:
|
|
44
|
+
resting = st["resting"]
|
|
45
|
+
return {
|
|
46
|
+
"status": "resting",
|
|
47
|
+
"oid": resting.get("oid", 0),
|
|
48
|
+
"asset": asset,
|
|
49
|
+
"side": side_str,
|
|
50
|
+
"size": size,
|
|
51
|
+
}
|
|
52
|
+
else:
|
|
53
|
+
return {"status": "unknown", "raw": st}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _log_trade(result: dict, order_type: str, price: str = ""):
|
|
57
|
+
"""Append trade record to data/trades.jsonl."""
|
|
58
|
+
trades_dir = Path("data")
|
|
59
|
+
trades_dir.mkdir(exist_ok=True)
|
|
60
|
+
record = {
|
|
61
|
+
"timestamp": format_timestamp(int(time.time() * 1000)),
|
|
62
|
+
"asset": result.get("asset", ""),
|
|
63
|
+
"side": result.get("side", ""),
|
|
64
|
+
"size": result.get("size", ""),
|
|
65
|
+
"price": result.get("avg_price", price),
|
|
66
|
+
"type": order_type,
|
|
67
|
+
"oid": result.get("oid", 0),
|
|
68
|
+
"status": result.get("status", ""),
|
|
69
|
+
}
|
|
70
|
+
with open(trades_dir / "trades.jsonl", "a") as f:
|
|
71
|
+
f.write(json.dumps(record, separators=(",", ":")) + "\n")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def order(
|
|
76
|
+
ctx: typer.Context,
|
|
77
|
+
asset: Annotated[str, typer.Argument(help="Asset name")],
|
|
78
|
+
side: Annotated[str, typer.Argument(help="buy/long or sell/short")],
|
|
79
|
+
size: Annotated[str, typer.Argument(help="Amount in base asset")],
|
|
80
|
+
price: Annotated[Optional[str], typer.Argument(help="Limit price")] = None,
|
|
81
|
+
market: Annotated[bool, typer.Option("--market", help="Market order (IOC)")] = False,
|
|
82
|
+
tif: Annotated[Optional[str], typer.Option("--tif", help="gtc|ioc|alo")] = None,
|
|
83
|
+
reduce_only: Annotated[bool, typer.Option("--reduce-only", help="Reduce only")] = False,
|
|
84
|
+
tp: Annotated[Optional[str], typer.Option("--tp", help="Take profit trigger price")] = None,
|
|
85
|
+
sl: Annotated[Optional[str], typer.Option("--sl", help="Stop loss trigger price")] = None,
|
|
86
|
+
cloid: Annotated[Optional[str], typer.Option("--cloid", help="Client order ID (hex)")] = None,
|
|
87
|
+
):
|
|
88
|
+
"""Place an order."""
|
|
89
|
+
opts = ctx.obj
|
|
90
|
+
|
|
91
|
+
side_lower = side.lower()
|
|
92
|
+
if side_lower not in SIDE_MAP:
|
|
93
|
+
die("input_error", f"Invalid side: {side}", hint="Use buy/long or sell/short")
|
|
94
|
+
|
|
95
|
+
is_buy = SIDE_MAP[side_lower]
|
|
96
|
+
sz = float(size)
|
|
97
|
+
|
|
98
|
+
exchange = get_exchange(opts["testnet"])
|
|
99
|
+
resolver = get_resolver(exchange.info)
|
|
100
|
+
resolved = resolver.resolve(asset)
|
|
101
|
+
|
|
102
|
+
from hyperliquid.utils.types import Cloid as SdkCloid
|
|
103
|
+
sdk_cloid = SdkCloid.from_str(cloid) if cloid else None
|
|
104
|
+
|
|
105
|
+
if market or price is None:
|
|
106
|
+
# Market order
|
|
107
|
+
resp = exchange.market_open(resolved, is_buy, sz, cloid=sdk_cloid)
|
|
108
|
+
result = _parse_order_response(resp, resolved, side_lower, size)
|
|
109
|
+
_log_trade(result, "market")
|
|
110
|
+
output(result, opts["fields"])
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# Limit order
|
|
114
|
+
px = float(price)
|
|
115
|
+
tif_val = (tif or "gtc").capitalize()
|
|
116
|
+
if tif_val == "Gtc":
|
|
117
|
+
tif_val = "Gtc"
|
|
118
|
+
elif tif_val == "Ioc":
|
|
119
|
+
tif_val = "Ioc"
|
|
120
|
+
elif tif_val == "Alo":
|
|
121
|
+
tif_val = "Alo"
|
|
122
|
+
order_type = {"limit": {"tif": tif_val}}
|
|
123
|
+
|
|
124
|
+
if tp or sl:
|
|
125
|
+
# Parent + TP/SL as grouped order
|
|
126
|
+
order_requests = [
|
|
127
|
+
{
|
|
128
|
+
"coin": resolved,
|
|
129
|
+
"is_buy": is_buy,
|
|
130
|
+
"sz": sz,
|
|
131
|
+
"limit_px": px,
|
|
132
|
+
"order_type": order_type,
|
|
133
|
+
"reduce_only": reduce_only,
|
|
134
|
+
"cloid": sdk_cloid,
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
if tp:
|
|
138
|
+
tp_type = {"trigger": {"triggerPx": float(tp), "isMarket": True, "tpsl": "tp"}}
|
|
139
|
+
order_requests.append({
|
|
140
|
+
"coin": resolved,
|
|
141
|
+
"is_buy": not is_buy,
|
|
142
|
+
"sz": sz,
|
|
143
|
+
"limit_px": float(tp),
|
|
144
|
+
"order_type": tp_type,
|
|
145
|
+
"reduce_only": True,
|
|
146
|
+
})
|
|
147
|
+
if sl:
|
|
148
|
+
sl_type = {"trigger": {"triggerPx": float(sl), "isMarket": True, "tpsl": "sl"}}
|
|
149
|
+
order_requests.append({
|
|
150
|
+
"coin": resolved,
|
|
151
|
+
"is_buy": not is_buy,
|
|
152
|
+
"sz": sz,
|
|
153
|
+
"limit_px": float(sl),
|
|
154
|
+
"order_type": sl_type,
|
|
155
|
+
"reduce_only": True,
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
resp = exchange.bulk_orders(
|
|
159
|
+
[exchange._order_request_to_wire(r) for r in order_requests]
|
|
160
|
+
if hasattr(exchange, "_order_request_to_wire")
|
|
161
|
+
else order_requests,
|
|
162
|
+
grouping="normalTpsl",
|
|
163
|
+
)
|
|
164
|
+
result = _parse_order_response(resp, resolved, side_lower, size)
|
|
165
|
+
_log_trade(result, "limit", price)
|
|
166
|
+
output(result, opts["fields"])
|
|
167
|
+
else:
|
|
168
|
+
resp = exchange.order(resolved, is_buy, sz, px, order_type, reduce_only, cloid=sdk_cloid)
|
|
169
|
+
result = _parse_order_response(resp, resolved, side_lower, size)
|
|
170
|
+
_log_trade(result, "limit", price)
|
|
171
|
+
output(result, opts["fields"])
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command()
|
|
175
|
+
def cancel(
|
|
176
|
+
ctx: typer.Context,
|
|
177
|
+
first: Annotated[str, typer.Argument(help="OID or asset name")],
|
|
178
|
+
second: Annotated[Optional[str], typer.Argument(help="OID (if first is asset)")] = None,
|
|
179
|
+
cloid: Annotated[Optional[str], typer.Option("--cloid", help="Cancel by client order ID")] = None,
|
|
180
|
+
asset_opt: Annotated[Optional[str], typer.Option("--asset", help="Asset for cloid cancel")] = None,
|
|
181
|
+
):
|
|
182
|
+
"""Cancel a specific order."""
|
|
183
|
+
opts = ctx.obj
|
|
184
|
+
exchange = get_exchange(opts["testnet"])
|
|
185
|
+
|
|
186
|
+
if cloid:
|
|
187
|
+
# Cancel by client order ID
|
|
188
|
+
if not asset_opt:
|
|
189
|
+
die("input_error", "Must specify --asset with --cloid", hint="hl cancel --cloid 0x... --asset BTC")
|
|
190
|
+
resolver = get_resolver(exchange.info)
|
|
191
|
+
resolved = resolver.resolve(asset_opt)
|
|
192
|
+
from hyperliquid.utils.types import Cloid as SdkCloid
|
|
193
|
+
resp = exchange.cancel_by_cloid(resolved, SdkCloid.from_str(cloid))
|
|
194
|
+
output({"status": "cancelled", "asset": resolved, "cloid": cloid}, opts["fields"])
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if second is not None:
|
|
198
|
+
# hl cancel BTC 77738308
|
|
199
|
+
resolver = get_resolver(exchange.info)
|
|
200
|
+
resolved = resolver.resolve(first)
|
|
201
|
+
oid = int(second)
|
|
202
|
+
exchange.cancel(resolved, oid)
|
|
203
|
+
output({"status": "cancelled", "oid": oid, "asset": resolved}, opts["fields"])
|
|
204
|
+
else:
|
|
205
|
+
# hl cancel 77738308 — auto-lookup asset from open orders
|
|
206
|
+
oid = int(first)
|
|
207
|
+
addr = get_address()
|
|
208
|
+
open_orders = exchange.info.open_orders(addr)
|
|
209
|
+
coin = None
|
|
210
|
+
for o in open_orders:
|
|
211
|
+
if o["oid"] == oid:
|
|
212
|
+
coin = o["coin"]
|
|
213
|
+
break
|
|
214
|
+
if not coin:
|
|
215
|
+
die("input_error", f"Order {oid} not found in open orders", hint="Pass asset explicitly: hl cancel BTC {oid}")
|
|
216
|
+
exchange.cancel(coin, oid)
|
|
217
|
+
output({"status": "cancelled", "oid": oid, "asset": coin}, opts["fields"])
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command(name="cancel-all")
|
|
221
|
+
def cancel_all(
|
|
222
|
+
ctx: typer.Context,
|
|
223
|
+
asset: Annotated[Optional[str], typer.Argument(help="Cancel only this asset's orders")] = None,
|
|
224
|
+
):
|
|
225
|
+
"""Cancel all open orders."""
|
|
226
|
+
opts = ctx.obj
|
|
227
|
+
exchange = get_exchange(opts["testnet"])
|
|
228
|
+
addr = get_address()
|
|
229
|
+
|
|
230
|
+
open_orders = exchange.info.open_orders(addr)
|
|
231
|
+
|
|
232
|
+
if asset:
|
|
233
|
+
resolver = get_resolver(exchange.info)
|
|
234
|
+
resolved = resolver.resolve(asset)
|
|
235
|
+
open_orders = [o for o in open_orders if o["coin"] == resolved]
|
|
236
|
+
|
|
237
|
+
if not open_orders:
|
|
238
|
+
output({"cancelled": 0, "assets": []}, opts["fields"])
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
cancel_requests = [{"coin": o["coin"], "oid": o["oid"]} for o in open_orders]
|
|
242
|
+
exchange.bulk_cancel(cancel_requests)
|
|
243
|
+
|
|
244
|
+
assets_cancelled = sorted(set(o["coin"] for o in open_orders))
|
|
245
|
+
output({"cancelled": len(open_orders), "assets": assets_cancelled}, opts["fields"])
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
@app.command()
|
|
249
|
+
def leverage(
|
|
250
|
+
ctx: typer.Context,
|
|
251
|
+
asset: Annotated[str, typer.Argument(help="Asset name")],
|
|
252
|
+
lev: Annotated[int, typer.Argument(help="Leverage (integer)")],
|
|
253
|
+
cross: Annotated[bool, typer.Option("--cross", help="Cross margin")] = False,
|
|
254
|
+
isolated: Annotated[bool, typer.Option("--isolated", help="Isolated margin")] = False,
|
|
255
|
+
):
|
|
256
|
+
"""Set leverage for an asset."""
|
|
257
|
+
opts = ctx.obj
|
|
258
|
+
|
|
259
|
+
if cross and isolated:
|
|
260
|
+
die("input_error", "Cannot specify both --cross and --isolated")
|
|
261
|
+
|
|
262
|
+
exchange = get_exchange(opts["testnet"])
|
|
263
|
+
resolver = get_resolver(exchange.info)
|
|
264
|
+
resolved = resolver.resolve(asset)
|
|
265
|
+
|
|
266
|
+
is_cross = not isolated # default is cross
|
|
267
|
+
if is_cross and ":" in resolved:
|
|
268
|
+
print("Warning: HIP-3 assets support isolated margin only. Using isolated.", file=sys.stderr)
|
|
269
|
+
is_cross = False
|
|
270
|
+
|
|
271
|
+
exchange.update_leverage(lev, resolved, is_cross)
|
|
272
|
+
|
|
273
|
+
output({
|
|
274
|
+
"asset": resolved,
|
|
275
|
+
"leverage": lev,
|
|
276
|
+
"margin_mode": "cross" if is_cross else "isolated",
|
|
277
|
+
}, opts["fields"])
|