hyperliquid-cli-python 0.1.5__tar.gz → 0.1.6__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_python-0.1.5/src/hyperliquid_cli_python.egg-info → hyperliquid_cli_python-0.1.6}/PKG-INFO +7 -3
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/README.md +6 -2
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/pyproject.toml +1 -1
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/argparse_main.py +4 -4
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/order.py +118 -5
- hyperliquid_cli_python-0.1.6/src/hl_cli/infra/twap_registry.py +144 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6/src/hyperliquid_cli_python.egg-info}/PKG-INFO +7 -3
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/SOURCES.txt +3 -1
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_order_testnet_routing.py +15 -0
- hyperliquid_cli_python-0.1.6/tests/test_twap_registry.py +44 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/LICENSE +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/setup.cfg +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/__init__.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/__init__.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/markets_tui.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/runtime.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/__init__.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/account.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/app.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/asset.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/common.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/markets.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/referral.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/__init__.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/context.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/order_config.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/testnet_policy.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/__init__.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/account_repo.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/db.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/db_crypto.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/paths.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/services/account_fetch.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/__init__.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/market_table.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/output.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/validators.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/watch.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/dependency_links.txt +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/entry_points.txt +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/requires.txt +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/top_level.txt +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_account_testnet_mode.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_accounts_networks.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_completion.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_json_patterns.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_markets_testnet_mode.py +0 -0
- {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_testnet_context.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperliquid-cli-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Python CLI for Hyperliquid DEX
|
|
5
5
|
Author: hyperliquid-cli contributors
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -72,7 +72,7 @@ python3 -m pip install --user -e .
|
|
|
72
72
|
Install from PyPI:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
pip install hyperliquid-cli-python
|
|
75
|
+
pip install --user hyperliquid-cli-python
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
After installation, the `hl` command is available:
|
|
@@ -223,7 +223,8 @@ Order side semantics:
|
|
|
223
223
|
### TWAP Orders
|
|
224
224
|
|
|
225
225
|
`hyperliquid-python-sdk` does not provide a high-level TWAP method, so this CLI signs and submits the official
|
|
226
|
-
`exchange` actions `twapOrder` / `twapCancel`.
|
|
226
|
+
`exchange` actions `twapOrder` / `twapCancel`. Successful submissions store the returned `twapId`
|
|
227
|
+
locally under `~/.hl/twap_orders.json` so the CLI can show and cancel tracked TWAPs later.
|
|
227
228
|
|
|
228
229
|
TWAP is perp-only, so use `long` / `short`.
|
|
229
230
|
|
|
@@ -239,6 +240,9 @@ hl order twap short 2.0 ETH 5,10 --randomize
|
|
|
239
240
|
|
|
240
241
|
# Cancel TWAP
|
|
241
242
|
hl order twap-cancel BTC 12345
|
|
243
|
+
|
|
244
|
+
# Or list tracked active TWAPs and pick one interactively
|
|
245
|
+
hl order twap-cancel
|
|
242
246
|
```
|
|
243
247
|
|
|
244
248
|
### Stake-Based Orders
|
|
@@ -57,7 +57,7 @@ python3 -m pip install --user -e .
|
|
|
57
57
|
Install from PyPI:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
|
-
pip install hyperliquid-cli-python
|
|
60
|
+
pip install --user hyperliquid-cli-python
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
After installation, the `hl` command is available:
|
|
@@ -208,7 +208,8 @@ Order side semantics:
|
|
|
208
208
|
### TWAP Orders
|
|
209
209
|
|
|
210
210
|
`hyperliquid-python-sdk` does not provide a high-level TWAP method, so this CLI signs and submits the official
|
|
211
|
-
`exchange` actions `twapOrder` / `twapCancel`.
|
|
211
|
+
`exchange` actions `twapOrder` / `twapCancel`. Successful submissions store the returned `twapId`
|
|
212
|
+
locally under `~/.hl/twap_orders.json` so the CLI can show and cancel tracked TWAPs later.
|
|
212
213
|
|
|
213
214
|
TWAP is perp-only, so use `long` / `short`.
|
|
214
215
|
|
|
@@ -224,6 +225,9 @@ hl order twap short 2.0 ETH 5,10 --randomize
|
|
|
224
225
|
|
|
225
226
|
# Cancel TWAP
|
|
226
227
|
hl order twap-cancel BTC 12345
|
|
228
|
+
|
|
229
|
+
# Or list tracked active TWAPs and pick one interactively
|
|
230
|
+
hl order twap-cancel
|
|
227
231
|
```
|
|
228
232
|
|
|
229
233
|
### Stake-Based Orders
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/argparse_main.py
RENAMED
|
@@ -318,11 +318,11 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
318
318
|
ord_twap_cancel = add_cmd_parser(
|
|
319
319
|
ord_sub,
|
|
320
320
|
"twap-cancel",
|
|
321
|
-
"Cancel native TWAP order",
|
|
322
|
-
["hl order twap-cancel BTC 12345"],
|
|
321
|
+
"Cancel native TWAP order (interactive if omitted)",
|
|
322
|
+
["hl order twap-cancel BTC 12345", "hl order twap-cancel"],
|
|
323
323
|
)
|
|
324
|
-
ord_twap_cancel.add_argument("coin")
|
|
325
|
-
ord_twap_cancel.add_argument("twap_id")
|
|
324
|
+
ord_twap_cancel.add_argument("coin", nargs="?")
|
|
325
|
+
ord_twap_cancel.add_argument("twap_id", nargs="?")
|
|
326
326
|
|
|
327
327
|
ord_cancel = add_cmd_parser(
|
|
328
328
|
ord_sub,
|
|
@@ -11,6 +11,12 @@ from ..cli.runtime import run_blocking
|
|
|
11
11
|
from ..core.context import CLIContext
|
|
12
12
|
from ..core.order_config import get_order_config, update_order_config
|
|
13
13
|
from ..core.testnet_policy import uses_main_perp_only
|
|
14
|
+
from ..infra.twap_registry import (
|
|
15
|
+
find_twap_order,
|
|
16
|
+
list_twap_orders,
|
|
17
|
+
mark_twap_cancelled,
|
|
18
|
+
register_twap_order,
|
|
19
|
+
)
|
|
14
20
|
from ..utils.output import out, out_success
|
|
15
21
|
from ..utils.validators import (
|
|
16
22
|
normalize_side,
|
|
@@ -486,6 +492,38 @@ def _fetch_orders(context: CLIContext, user: str) -> list[dict[str, Any]]:
|
|
|
486
492
|
for o in orders
|
|
487
493
|
]
|
|
488
494
|
|
|
495
|
+
def _network_name(context: CLIContext) -> str:
|
|
496
|
+
return "testnet" if context.config.testnet else "mainnet"
|
|
497
|
+
|
|
498
|
+
def _extract_twap_id(response: dict[str, Any]) -> Optional[int]:
|
|
499
|
+
status = response.get("response", {}).get("data", {}).get("status", {})
|
|
500
|
+
if not isinstance(status, dict):
|
|
501
|
+
return None
|
|
502
|
+
running = status.get("running")
|
|
503
|
+
if not isinstance(running, dict):
|
|
504
|
+
return None
|
|
505
|
+
try:
|
|
506
|
+
return int(running["twapId"])
|
|
507
|
+
except (KeyError, TypeError, ValueError):
|
|
508
|
+
return None
|
|
509
|
+
|
|
510
|
+
def _render_twap_orders(title: str, records: list[Any]) -> None:
|
|
511
|
+
_render_table(
|
|
512
|
+
title,
|
|
513
|
+
["TWAP ID", "Coin", "Side", "Total Size", "Minutes", "Submitted"],
|
|
514
|
+
[
|
|
515
|
+
[
|
|
516
|
+
record.twap_id,
|
|
517
|
+
record.coin,
|
|
518
|
+
record.side,
|
|
519
|
+
record.total_size,
|
|
520
|
+
record.duration_minutes,
|
|
521
|
+
record.submitted_at,
|
|
522
|
+
]
|
|
523
|
+
for record in records
|
|
524
|
+
],
|
|
525
|
+
)
|
|
526
|
+
|
|
489
527
|
@cli_command
|
|
490
528
|
def order_ls(
|
|
491
529
|
ctx: Any,
|
|
@@ -494,6 +532,7 @@ def order_ls(
|
|
|
494
532
|
) -> None:
|
|
495
533
|
context = _ctx(ctx)
|
|
496
534
|
address = user if user else context.get_wallet_address()
|
|
535
|
+
tracked_twaps = list_twap_orders(network=_network_name(context), user=address)
|
|
497
536
|
if watch:
|
|
498
537
|
watch_loop(
|
|
499
538
|
lambda: _fetch_orders(context, address),
|
|
@@ -505,7 +544,16 @@ def order_ls(
|
|
|
505
544
|
as_json=_json(ctx),
|
|
506
545
|
)
|
|
507
546
|
return
|
|
508
|
-
|
|
547
|
+
orders = _fetch_orders(context, address)
|
|
548
|
+
if _json(ctx):
|
|
549
|
+
if tracked_twaps:
|
|
550
|
+
out({"openOrders": orders, "trackedTwaps": [record.__dict__ for record in tracked_twaps]}, True)
|
|
551
|
+
else:
|
|
552
|
+
out(orders, True)
|
|
553
|
+
else:
|
|
554
|
+
if tracked_twaps:
|
|
555
|
+
_render_twap_orders("Tracked TWAP Orders", tracked_twaps)
|
|
556
|
+
out(orders, False)
|
|
509
557
|
_done(ctx)
|
|
510
558
|
|
|
511
559
|
@cli_command
|
|
@@ -790,6 +838,7 @@ def order_twap(
|
|
|
790
838
|
"twap": {
|
|
791
839
|
"side": "buy" if is_buy else "sell",
|
|
792
840
|
"coin": coin,
|
|
841
|
+
"resolvedCoin": resolved_coin,
|
|
793
842
|
"totalSize": total_size,
|
|
794
843
|
"stake": stake,
|
|
795
844
|
"durationMinutes": minutes,
|
|
@@ -800,6 +849,23 @@ def order_twap(
|
|
|
800
849
|
"response": response,
|
|
801
850
|
}
|
|
802
851
|
}
|
|
852
|
+
twap_id = _extract_twap_id(response)
|
|
853
|
+
if twap_id is not None:
|
|
854
|
+
# TODO: Replace the local TWAP registry with an official info endpoint once
|
|
855
|
+
# Hyperliquid exposes an API for listing/retrieving active TWAP orders by twapId.
|
|
856
|
+
register_twap_order(
|
|
857
|
+
network=_network_name(context),
|
|
858
|
+
user=context.get_wallet_address(),
|
|
859
|
+
coin=coin,
|
|
860
|
+
resolved_coin=resolved_coin,
|
|
861
|
+
twap_id=twap_id,
|
|
862
|
+
side="buy" if is_buy else "sell",
|
|
863
|
+
total_size=total_size,
|
|
864
|
+
duration_minutes=minutes,
|
|
865
|
+
randomize=randomize,
|
|
866
|
+
reduce_only=reduce_only,
|
|
867
|
+
)
|
|
868
|
+
result["twap"]["twapId"] = twap_id
|
|
803
869
|
if _json(ctx):
|
|
804
870
|
out(result, True)
|
|
805
871
|
else:
|
|
@@ -815,14 +881,61 @@ def order_twap(
|
|
|
815
881
|
print(f"Total size: {total_size} {coin}")
|
|
816
882
|
print(f"Duration: {minutes} min")
|
|
817
883
|
print(f"Randomize: {'on' if randomize else 'off'}")
|
|
884
|
+
if twap_id is not None:
|
|
885
|
+
print(f"TWAP ID: {twap_id}")
|
|
886
|
+
print("Manage it with 'hl order ls' or 'hl order twap-cancel'.")
|
|
818
887
|
_done(ctx)
|
|
819
888
|
|
|
820
889
|
@cli_command
|
|
821
|
-
def order_twap_cancel(ctx: Any, coin: str, twap_id: str) -> None:
|
|
890
|
+
def order_twap_cancel(ctx: Any, coin: Optional[str] = None, twap_id: Optional[str] = None) -> None:
|
|
822
891
|
context = _ctx(ctx)
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
892
|
+
address = context.get_wallet_address()
|
|
893
|
+
if coin is not None and twap_id is not None:
|
|
894
|
+
resolved_coin = coin
|
|
895
|
+
twap_num = validate_positive_integer(twap_id, "twap_id")
|
|
896
|
+
else:
|
|
897
|
+
records = list_twap_orders(network=_network_name(context), user=address)
|
|
898
|
+
if coin is not None:
|
|
899
|
+
records = [record for record in records if coin in {record.coin, record.resolved_coin}]
|
|
900
|
+
if not records:
|
|
901
|
+
raise RuntimeError("No tracked active TWAP orders found")
|
|
902
|
+
if twap_id is not None:
|
|
903
|
+
twap_num = validate_positive_integer(twap_id, "twap_id")
|
|
904
|
+
record = find_twap_order(
|
|
905
|
+
network=_network_name(context),
|
|
906
|
+
user=address,
|
|
907
|
+
twap_id=twap_num,
|
|
908
|
+
coin=coin,
|
|
909
|
+
)
|
|
910
|
+
if record is None:
|
|
911
|
+
raise RuntimeError(f"Tracked TWAP {twap_num} not found")
|
|
912
|
+
resolved_coin = record.resolved_coin
|
|
913
|
+
elif _json(ctx):
|
|
914
|
+
latest = records[0]
|
|
915
|
+
resolved_coin = latest.resolved_coin
|
|
916
|
+
twap_num = latest.twap_id
|
|
917
|
+
else:
|
|
918
|
+
_render_twap_orders("Tracked TWAP Orders", records)
|
|
919
|
+
selected = input("Select TWAP ID to cancel: ").strip()
|
|
920
|
+
twap_num = validate_positive_integer(selected, "twap_id")
|
|
921
|
+
record = find_twap_order(
|
|
922
|
+
network=_network_name(context),
|
|
923
|
+
user=address,
|
|
924
|
+
twap_id=twap_num,
|
|
925
|
+
)
|
|
926
|
+
if record is None:
|
|
927
|
+
raise RuntimeError(f"Tracked TWAP {twap_num} not found")
|
|
928
|
+
resolved_coin = record.resolved_coin
|
|
929
|
+
response = _cancel_native_twap(context=context, coin=resolved_coin, twap_id=twap_num)
|
|
930
|
+
status = response.get("response", {}).get("data", {}).get("status", {})
|
|
931
|
+
if not (isinstance(status, dict) and status.get("error")):
|
|
932
|
+
mark_twap_cancelled(
|
|
933
|
+
network=_network_name(context),
|
|
934
|
+
user=address,
|
|
935
|
+
twap_id=twap_num,
|
|
936
|
+
coin=resolved_coin,
|
|
937
|
+
)
|
|
938
|
+
out({"twapCancel": {"coin": resolved_coin, "twapId": twap_num, "response": response}}, _json(ctx))
|
|
826
939
|
_done(ctx)
|
|
827
940
|
|
|
828
941
|
@cli_command
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from dataclasses import asdict, dataclass
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from .paths import HL_DIR
|
|
7
|
+
|
|
8
|
+
TWAP_REGISTRY_PATH = HL_DIR / "twap_orders.json"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class TwapRecord:
|
|
13
|
+
network: str
|
|
14
|
+
user: str
|
|
15
|
+
coin: str
|
|
16
|
+
resolved_coin: str
|
|
17
|
+
twap_id: int
|
|
18
|
+
side: str
|
|
19
|
+
total_size: float
|
|
20
|
+
duration_minutes: int
|
|
21
|
+
randomize: bool
|
|
22
|
+
reduce_only: bool
|
|
23
|
+
submitted_at: str
|
|
24
|
+
status: str = "active"
|
|
25
|
+
cancelled_at: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _utc_now() -> str:
|
|
29
|
+
return datetime.now(timezone.utc).isoformat()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _load_all() -> list[TwapRecord]:
|
|
33
|
+
if not TWAP_REGISTRY_PATH.exists():
|
|
34
|
+
return []
|
|
35
|
+
try:
|
|
36
|
+
raw = json.loads(TWAP_REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
37
|
+
except Exception:
|
|
38
|
+
return []
|
|
39
|
+
if not isinstance(raw, list):
|
|
40
|
+
return []
|
|
41
|
+
records: list[TwapRecord] = []
|
|
42
|
+
for item in raw:
|
|
43
|
+
if not isinstance(item, dict):
|
|
44
|
+
continue
|
|
45
|
+
try:
|
|
46
|
+
records.append(TwapRecord(**item))
|
|
47
|
+
except TypeError:
|
|
48
|
+
continue
|
|
49
|
+
return records
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _save_all(records: list[TwapRecord]) -> None:
|
|
53
|
+
HL_DIR.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
TWAP_REGISTRY_PATH.write_text(
|
|
55
|
+
json.dumps([asdict(record) for record in records], ensure_ascii=False, indent=2),
|
|
56
|
+
encoding="utf-8",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def register_twap_order(
|
|
61
|
+
*,
|
|
62
|
+
network: str,
|
|
63
|
+
user: str,
|
|
64
|
+
coin: str,
|
|
65
|
+
resolved_coin: str,
|
|
66
|
+
twap_id: int,
|
|
67
|
+
side: str,
|
|
68
|
+
total_size: float,
|
|
69
|
+
duration_minutes: int,
|
|
70
|
+
randomize: bool,
|
|
71
|
+
reduce_only: bool,
|
|
72
|
+
) -> None:
|
|
73
|
+
records = _load_all()
|
|
74
|
+
records = [
|
|
75
|
+
record
|
|
76
|
+
for record in records
|
|
77
|
+
if not (
|
|
78
|
+
record.network == network
|
|
79
|
+
and record.user.lower() == user.lower()
|
|
80
|
+
and record.twap_id == twap_id
|
|
81
|
+
and record.resolved_coin == resolved_coin
|
|
82
|
+
)
|
|
83
|
+
]
|
|
84
|
+
records.append(
|
|
85
|
+
TwapRecord(
|
|
86
|
+
network=network,
|
|
87
|
+
user=user,
|
|
88
|
+
coin=coin,
|
|
89
|
+
resolved_coin=resolved_coin,
|
|
90
|
+
twap_id=twap_id,
|
|
91
|
+
side=side,
|
|
92
|
+
total_size=total_size,
|
|
93
|
+
duration_minutes=duration_minutes,
|
|
94
|
+
randomize=randomize,
|
|
95
|
+
reduce_only=reduce_only,
|
|
96
|
+
submitted_at=_utc_now(),
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
_save_all(records)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def list_twap_orders(*, network: str, user: str, active_only: bool = True) -> list[TwapRecord]:
|
|
103
|
+
records = [
|
|
104
|
+
record
|
|
105
|
+
for record in _load_all()
|
|
106
|
+
if record.network == network and record.user.lower() == user.lower()
|
|
107
|
+
]
|
|
108
|
+
if active_only:
|
|
109
|
+
records = [record for record in records if record.status == "active"]
|
|
110
|
+
records.sort(key=lambda record: record.submitted_at, reverse=True)
|
|
111
|
+
return records
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_twap_order(
|
|
115
|
+
*,
|
|
116
|
+
network: str,
|
|
117
|
+
user: str,
|
|
118
|
+
twap_id: int,
|
|
119
|
+
coin: Optional[str] = None,
|
|
120
|
+
) -> Optional[TwapRecord]:
|
|
121
|
+
for record in list_twap_orders(network=network, user=user, active_only=False):
|
|
122
|
+
if record.twap_id != twap_id:
|
|
123
|
+
continue
|
|
124
|
+
if coin and coin not in {record.coin, record.resolved_coin}:
|
|
125
|
+
continue
|
|
126
|
+
return record
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def mark_twap_cancelled(*, network: str, user: str, twap_id: int, coin: Optional[str] = None) -> None:
|
|
131
|
+
records = _load_all()
|
|
132
|
+
updated = False
|
|
133
|
+
for record in records:
|
|
134
|
+
if record.network != network or record.user.lower() != user.lower():
|
|
135
|
+
continue
|
|
136
|
+
if record.twap_id != twap_id:
|
|
137
|
+
continue
|
|
138
|
+
if coin and coin not in {record.coin, record.resolved_coin}:
|
|
139
|
+
continue
|
|
140
|
+
record.status = "cancelled"
|
|
141
|
+
record.cancelled_at = _utc_now()
|
|
142
|
+
updated = True
|
|
143
|
+
if updated:
|
|
144
|
+
_save_all(records)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperliquid-cli-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Python CLI for Hyperliquid DEX
|
|
5
5
|
Author: hyperliquid-cli contributors
|
|
6
6
|
License-Expression: BSD-2-Clause
|
|
@@ -72,7 +72,7 @@ python3 -m pip install --user -e .
|
|
|
72
72
|
Install from PyPI:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
pip install hyperliquid-cli-python
|
|
75
|
+
pip install --user hyperliquid-cli-python
|
|
76
76
|
```
|
|
77
77
|
|
|
78
78
|
After installation, the `hl` command is available:
|
|
@@ -223,7 +223,8 @@ Order side semantics:
|
|
|
223
223
|
### TWAP Orders
|
|
224
224
|
|
|
225
225
|
`hyperliquid-python-sdk` does not provide a high-level TWAP method, so this CLI signs and submits the official
|
|
226
|
-
`exchange` actions `twapOrder` / `twapCancel`.
|
|
226
|
+
`exchange` actions `twapOrder` / `twapCancel`. Successful submissions store the returned `twapId`
|
|
227
|
+
locally under `~/.hl/twap_orders.json` so the CLI can show and cancel tracked TWAPs later.
|
|
227
228
|
|
|
228
229
|
TWAP is perp-only, so use `long` / `short`.
|
|
229
230
|
|
|
@@ -239,6 +240,9 @@ hl order twap short 2.0 ETH 5,10 --randomize
|
|
|
239
240
|
|
|
240
241
|
# Cancel TWAP
|
|
241
242
|
hl order twap-cancel BTC 12345
|
|
243
|
+
|
|
244
|
+
# Or list tracked active TWAPs and pick one interactively
|
|
245
|
+
hl order twap-cancel
|
|
242
246
|
```
|
|
243
247
|
|
|
244
248
|
### Stake-Based Orders
|
|
@@ -23,6 +23,7 @@ src/hl_cli/infra/account_repo.py
|
|
|
23
23
|
src/hl_cli/infra/db.py
|
|
24
24
|
src/hl_cli/infra/db_crypto.py
|
|
25
25
|
src/hl_cli/infra/paths.py
|
|
26
|
+
src/hl_cli/infra/twap_registry.py
|
|
26
27
|
src/hl_cli/services/account_fetch.py
|
|
27
28
|
src/hl_cli/utils/__init__.py
|
|
28
29
|
src/hl_cli/utils/market_table.py
|
|
@@ -41,4 +42,5 @@ tests/test_completion.py
|
|
|
41
42
|
tests/test_json_patterns.py
|
|
42
43
|
tests/test_markets_testnet_mode.py
|
|
43
44
|
tests/test_order_testnet_routing.py
|
|
44
|
-
tests/test_testnet_context.py
|
|
45
|
+
tests/test_testnet_context.py
|
|
46
|
+
tests/test_twap_registry.py
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_order_testnet_routing.py
RENAMED
|
@@ -3,6 +3,7 @@ from unittest.mock import patch
|
|
|
3
3
|
|
|
4
4
|
from hl_cli.commands.order import (
|
|
5
5
|
_close_position_perp_dexs,
|
|
6
|
+
_extract_twap_id,
|
|
6
7
|
_resolve_coin_for_side,
|
|
7
8
|
_resolve_spot_coin,
|
|
8
9
|
_validate_side_mode_args,
|
|
@@ -78,5 +79,19 @@ class OrderTestnetRoutingTests(unittest.TestCase):
|
|
|
78
79
|
context.info = _FakeInfo()
|
|
79
80
|
self.assertEqual(_resolve_spot_coin(context, "@1035"), "@1035")
|
|
80
81
|
|
|
82
|
+
def test_extract_twap_id_from_running_status(self):
|
|
83
|
+
response = {
|
|
84
|
+
"response": {
|
|
85
|
+
"data": {
|
|
86
|
+
"status": {
|
|
87
|
+
"running": {
|
|
88
|
+
"twapId": 12345,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
self.assertEqual(_extract_twap_id(response), 12345)
|
|
95
|
+
|
|
81
96
|
if __name__ == "__main__":
|
|
82
97
|
unittest.main()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import unittest
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from hl_cli.infra import twap_registry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TwapRegistryTests(unittest.TestCase):
|
|
11
|
+
def test_register_list_find_and_cancel(self):
|
|
12
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
13
|
+
path = Path(tmp) / "twap_orders.json"
|
|
14
|
+
with patch.object(twap_registry, "TWAP_REGISTRY_PATH", path):
|
|
15
|
+
twap_registry.register_twap_order(
|
|
16
|
+
network="testnet",
|
|
17
|
+
user="0xabc",
|
|
18
|
+
coin="BTC",
|
|
19
|
+
resolved_coin="BTC",
|
|
20
|
+
twap_id=123,
|
|
21
|
+
side="buy",
|
|
22
|
+
total_size=1.25,
|
|
23
|
+
duration_minutes=10,
|
|
24
|
+
randomize=False,
|
|
25
|
+
reduce_only=False,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
active = twap_registry.list_twap_orders(network="testnet", user="0xabc")
|
|
29
|
+
self.assertEqual(len(active), 1)
|
|
30
|
+
self.assertEqual(active[0].twap_id, 123)
|
|
31
|
+
|
|
32
|
+
record = twap_registry.find_twap_order(network="testnet", user="0xabc", twap_id=123)
|
|
33
|
+
self.assertIsNotNone(record)
|
|
34
|
+
self.assertEqual(record.resolved_coin, "BTC")
|
|
35
|
+
|
|
36
|
+
twap_registry.mark_twap_cancelled(network="testnet", user="0xabc", twap_id=123)
|
|
37
|
+
self.assertEqual(twap_registry.list_twap_orders(network="testnet", user="0xabc"), [])
|
|
38
|
+
|
|
39
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
40
|
+
self.assertEqual(raw[0]["status"], "cancelled")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/__init__.py
RENAMED
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/account.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/markets.py
RENAMED
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/referral.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/order_config.py
RENAMED
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/testnet_policy.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/account_repo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/services/account_fetch.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/market_table.py
RENAMED
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/validators.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_account_testnet_mode.py
RENAMED
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_accounts_networks.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_markets_testnet_mode.py
RENAMED
|
File without changes
|
|
File without changes
|