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.
Files changed (48) hide show
  1. {hyperliquid_cli_python-0.1.5/src/hyperliquid_cli_python.egg-info → hyperliquid_cli_python-0.1.6}/PKG-INFO +7 -3
  2. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/README.md +6 -2
  3. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/pyproject.toml +1 -1
  4. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/argparse_main.py +4 -4
  5. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/order.py +118 -5
  6. hyperliquid_cli_python-0.1.6/src/hl_cli/infra/twap_registry.py +144 -0
  7. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6/src/hyperliquid_cli_python.egg-info}/PKG-INFO +7 -3
  8. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/SOURCES.txt +3 -1
  9. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_order_testnet_routing.py +15 -0
  10. hyperliquid_cli_python-0.1.6/tests/test_twap_registry.py +44 -0
  11. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/LICENSE +0 -0
  12. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/setup.cfg +0 -0
  13. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/__init__.py +0 -0
  14. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/__init__.py +0 -0
  15. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/markets_tui.py +0 -0
  16. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/cli/runtime.py +0 -0
  17. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/__init__.py +0 -0
  18. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/account.py +0 -0
  19. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/app.py +0 -0
  20. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/asset.py +0 -0
  21. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/common.py +0 -0
  22. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/markets.py +0 -0
  23. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/commands/referral.py +0 -0
  24. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/__init__.py +0 -0
  25. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/context.py +0 -0
  26. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/order_config.py +0 -0
  27. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/core/testnet_policy.py +0 -0
  28. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/__init__.py +0 -0
  29. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/account_repo.py +0 -0
  30. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/db.py +0 -0
  31. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/db_crypto.py +0 -0
  32. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/infra/paths.py +0 -0
  33. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/services/account_fetch.py +0 -0
  34. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/__init__.py +0 -0
  35. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/market_table.py +0 -0
  36. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/output.py +0 -0
  37. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/validators.py +0 -0
  38. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hl_cli/utils/watch.py +0 -0
  39. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/dependency_links.txt +0 -0
  40. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/entry_points.txt +0 -0
  41. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/requires.txt +0 -0
  42. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/src/hyperliquid_cli_python.egg-info/top_level.txt +0 -0
  43. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_account_testnet_mode.py +0 -0
  44. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_accounts_networks.py +0 -0
  45. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_completion.py +0 -0
  46. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_json_patterns.py +0 -0
  47. {hyperliquid_cli_python-0.1.5 → hyperliquid_cli_python-0.1.6}/tests/test_markets_testnet_mode.py +0 -0
  48. {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.5
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "hyperliquid-cli-python"
7
- version = "0.1.5"
7
+ version = "0.1.6"
8
8
  description = "Python CLI for Hyperliquid DEX"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
- out(_fetch_orders(context, address), _json(ctx))
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
- twap_num = validate_positive_integer(twap_id, "twap_id")
824
- response = _cancel_native_twap(context=context, coin=coin, twap_id=twap_num)
825
- out({"twapCancel": {"coin": coin, "twapId": twap_num, "response": response}}, _json(ctx))
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.5
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
@@ -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()