hyperliquid-cli-python 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hl_cli/__init__.py +1 -0
- hl_cli/cli/__init__.py +1 -0
- hl_cli/cli/argparse_main.py +814 -0
- hl_cli/cli/markets_tui.py +399 -0
- hl_cli/cli/runtime.py +82 -0
- hl_cli/commands/__init__.py +1 -0
- hl_cli/commands/app.py +1081 -0
- hl_cli/commands/order.py +918 -0
- hl_cli/core/__init__.py +1 -0
- hl_cli/core/context.py +156 -0
- hl_cli/core/order_config.py +23 -0
- hl_cli/infra/__init__.py +1 -0
- hl_cli/infra/db.py +277 -0
- hl_cli/infra/paths.py +5 -0
- hl_cli/utils/__init__.py +1 -0
- hl_cli/utils/market_table.py +66 -0
- hl_cli/utils/output.py +476 -0
- hl_cli/utils/validators.py +45 -0
- hl_cli/utils/watch.py +28 -0
- hyperliquid_cli_python-0.1.0.dist-info/METADATA +269 -0
- hyperliquid_cli_python-0.1.0.dist-info/RECORD +25 -0
- hyperliquid_cli_python-0.1.0.dist-info/WHEEL +5 -0
- hyperliquid_cli_python-0.1.0.dist-info/entry_points.txt +2 -0
- hyperliquid_cli_python-0.1.0.dist-info/licenses/LICENSE +32 -0
- hyperliquid_cli_python-0.1.0.dist-info/top_level.txt +1 -0
hl_cli/commands/order.py
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from decimal import Decimal, ROUND_DOWN
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
from hyperliquid.utils.constants import MAINNET_API_URL
|
|
7
|
+
from hyperliquid.utils.signing import float_to_wire, get_timestamp_ms, sign_l1_action
|
|
8
|
+
|
|
9
|
+
from ..cli.runtime import cli_command, cli_context, confirm, finish_command, json_output_enabled, render_table
|
|
10
|
+
from ..cli.runtime import run_blocking
|
|
11
|
+
from ..core.context import CLIContext
|
|
12
|
+
from ..core.order_config import get_order_config, update_order_config
|
|
13
|
+
from ..utils.output import out, out_success
|
|
14
|
+
from ..utils.validators import (
|
|
15
|
+
normalize_side,
|
|
16
|
+
normalize_tif,
|
|
17
|
+
validate_positive_integer,
|
|
18
|
+
validate_positive_number,
|
|
19
|
+
)
|
|
20
|
+
from ..utils.watch import watch_loop
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _ctx(ctx: Any) -> CLIContext:
|
|
24
|
+
return cli_context(ctx)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _json(ctx: Any) -> bool:
|
|
28
|
+
return json_output_enabled(ctx)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _done(ctx: Any) -> None:
|
|
32
|
+
finish_command(ctx)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _wallet_perp_dexs_for_coin(coin: str) -> Optional[list[str]]:
|
|
36
|
+
if ":" not in coin:
|
|
37
|
+
return None
|
|
38
|
+
return [coin.split(":", 1)[0]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _confirm(message: str, default: bool = False) -> bool:
|
|
42
|
+
return confirm(message, default)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _render_table(title: str, columns: list[str], rows: list[list[Any]]) -> None:
|
|
46
|
+
render_table(title, columns, rows)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_usd(value: str | float | int | None) -> str:
|
|
50
|
+
try:
|
|
51
|
+
n = float(value) # type: ignore[arg-type]
|
|
52
|
+
return f"${n:,.2f}"
|
|
53
|
+
except Exception:
|
|
54
|
+
return f"${value}" if value is not None else "-"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _extract_statuses(result: dict[str, Any]) -> list[dict[str, Any] | str]:
|
|
58
|
+
try:
|
|
59
|
+
statuses = result.get("response", {}).get("data", {}).get("statuses", [])
|
|
60
|
+
if isinstance(statuses, list):
|
|
61
|
+
return statuses
|
|
62
|
+
return []
|
|
63
|
+
except Exception:
|
|
64
|
+
return []
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _print_leverage_update(lev_result: Optional[dict[str, Any]], coin: str, leverage: Optional[int], is_cross: bool) -> None:
|
|
68
|
+
if not lev_result:
|
|
69
|
+
return
|
|
70
|
+
if lev_result.get("status") == "ok":
|
|
71
|
+
if leverage is not None:
|
|
72
|
+
print(f"⚙️ Leverage set: {coin} {leverage}x ({'cross' if is_cross else 'isolated'})")
|
|
73
|
+
else:
|
|
74
|
+
print(f"⚠️ Leverage update failed: {lev_result.get('response')}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _print_order_feedback(
|
|
78
|
+
*,
|
|
79
|
+
result: dict[str, Any],
|
|
80
|
+
coin: str,
|
|
81
|
+
side: str,
|
|
82
|
+
order_kind: str,
|
|
83
|
+
stake: Optional[float] = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
statuses = _extract_statuses(result)
|
|
86
|
+
if not statuses:
|
|
87
|
+
print("ℹ️ Request sent.")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
first_error = None
|
|
91
|
+
first_filled = None
|
|
92
|
+
first_resting = None
|
|
93
|
+
for s in statuses:
|
|
94
|
+
if isinstance(s, dict) and "error" in s and first_error is None:
|
|
95
|
+
first_error = str(s["error"])
|
|
96
|
+
if isinstance(s, dict) and "filled" in s and first_filled is None:
|
|
97
|
+
first_filled = s["filled"]
|
|
98
|
+
if isinstance(s, dict) and "resting" in s and first_resting is None:
|
|
99
|
+
first_resting = s["resting"]
|
|
100
|
+
|
|
101
|
+
if first_error is not None:
|
|
102
|
+
print("❌ Order rejected")
|
|
103
|
+
print(f"\nReason: {first_error}")
|
|
104
|
+
if stake is not None:
|
|
105
|
+
print(f"Your stake (margin): {_format_usd(stake)}")
|
|
106
|
+
if "minimum value" in first_error.lower():
|
|
107
|
+
print("\nTip: Increase --stake or --leverage so position value is at least $10.")
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
if first_filled is not None:
|
|
111
|
+
print(f"✅ {order_kind} order executed")
|
|
112
|
+
print(f"\nAsset: {coin}")
|
|
113
|
+
print(f"Side: {side.upper()}")
|
|
114
|
+
print(f"Filled size: {first_filled.get('totalSz')} {coin}")
|
|
115
|
+
print(f"Average price: {_format_usd(first_filled.get('avgPx'))}")
|
|
116
|
+
print(f"Order ID: {first_filled.get('oid')}")
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
if first_resting is not None:
|
|
120
|
+
print(f"✅ {order_kind} order placed")
|
|
121
|
+
print(f"\nAsset: {coin}")
|
|
122
|
+
print(f"Side: {side.upper()}")
|
|
123
|
+
print(f"Order ID: {first_resting.get('oid')}")
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
print("ℹ️ Request completed.")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _parse_twap_interval(value: str) -> tuple[int, int]:
|
|
130
|
+
parts = [x.strip() for x in value.split(",")]
|
|
131
|
+
if len(parts) == 1:
|
|
132
|
+
minutes = int(parts[0])
|
|
133
|
+
if minutes <= 0:
|
|
134
|
+
raise ValueError("minutes must be a positive integer")
|
|
135
|
+
return minutes, 1
|
|
136
|
+
if len(parts) == 2:
|
|
137
|
+
minutes = int(parts[0])
|
|
138
|
+
orders = int(parts[1])
|
|
139
|
+
if minutes <= 0 or orders <= 0:
|
|
140
|
+
raise ValueError("minutes and orders must be positive integers")
|
|
141
|
+
return minutes * orders, orders
|
|
142
|
+
raise ValueError("interval must be '<minutes>' or '<slice_minutes>,<orders>' (e.g. 30 or 5,10)")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _get_max_leverage_for_coin(context: CLIContext, coin: str) -> int:
|
|
146
|
+
info = context.get_public_client()
|
|
147
|
+
|
|
148
|
+
dex = ""
|
|
149
|
+
search_names = [coin]
|
|
150
|
+
if ":" in coin:
|
|
151
|
+
dex = coin.split(":", 1)[0]
|
|
152
|
+
base = coin.split(":", 1)[1]
|
|
153
|
+
search_names.append(base)
|
|
154
|
+
|
|
155
|
+
meta = info.meta(dex=dex)
|
|
156
|
+
for m in meta.get("universe", []):
|
|
157
|
+
if m.get("name") in search_names:
|
|
158
|
+
max_lev = m.get("maxLeverage")
|
|
159
|
+
if max_lev is None:
|
|
160
|
+
continue
|
|
161
|
+
return int(max_lev)
|
|
162
|
+
raise RuntimeError(f"Could not resolve max leverage for {coin}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _is_invalid_leverage_response(resp: Any) -> bool:
|
|
166
|
+
if not isinstance(resp, dict):
|
|
167
|
+
return False
|
|
168
|
+
if str(resp.get("status", "")).lower() != "err":
|
|
169
|
+
return False
|
|
170
|
+
return "invalid leverage value" in str(resp.get("response", "")).lower()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _update_leverage_with_fallback(
|
|
174
|
+
*,
|
|
175
|
+
context: CLIContext,
|
|
176
|
+
coin: str,
|
|
177
|
+
leverage: int,
|
|
178
|
+
is_cross: bool,
|
|
179
|
+
emit_warning: bool = True,
|
|
180
|
+
) -> dict[str, Any]:
|
|
181
|
+
wallet = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(coin))
|
|
182
|
+
result = wallet.update_leverage(leverage, coin, is_cross=is_cross)
|
|
183
|
+
if not _is_invalid_leverage_response(result):
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
max_lev = _get_max_leverage_for_coin(context, coin)
|
|
187
|
+
if emit_warning:
|
|
188
|
+
print(
|
|
189
|
+
f"Warning: Invalid leverage value ({leverage}) for {coin}. "
|
|
190
|
+
f"Retrying with max leverage {max_lev}."
|
|
191
|
+
)
|
|
192
|
+
return wallet.update_leverage(max_lev, coin, is_cross=is_cross)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _maybe_update_leverage(
|
|
196
|
+
*,
|
|
197
|
+
context: CLIContext,
|
|
198
|
+
coin: str,
|
|
199
|
+
leverage: Optional[int],
|
|
200
|
+
cross: bool,
|
|
201
|
+
isolated: bool,
|
|
202
|
+
emit_warning: bool = True,
|
|
203
|
+
) -> Optional[dict[str, Any]]:
|
|
204
|
+
if cross and isolated:
|
|
205
|
+
raise RuntimeError("Use only one of --cross or --isolated")
|
|
206
|
+
if leverage is None:
|
|
207
|
+
if cross or isolated:
|
|
208
|
+
raise RuntimeError("--cross/--isolated requires --leverage")
|
|
209
|
+
return None
|
|
210
|
+
if leverage <= 0:
|
|
211
|
+
raise RuntimeError("leverage must be a positive integer")
|
|
212
|
+
is_cross = cross or not isolated
|
|
213
|
+
return _update_leverage_with_fallback(
|
|
214
|
+
context=context,
|
|
215
|
+
coin=coin,
|
|
216
|
+
leverage=leverage,
|
|
217
|
+
is_cross=is_cross,
|
|
218
|
+
emit_warning=emit_warning,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _normalize_size_for_coin(context: CLIContext, coin: str, raw_size: float) -> float:
|
|
223
|
+
if raw_size <= 0:
|
|
224
|
+
raise RuntimeError("size must be a positive number")
|
|
225
|
+
exchange = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(coin))
|
|
226
|
+
asset = exchange.info.name_to_asset(coin)
|
|
227
|
+
sz_decimals = int(exchange.info.asset_to_sz_decimals[asset])
|
|
228
|
+
|
|
229
|
+
q = Decimal(1).scaleb(-sz_decimals)
|
|
230
|
+
d = Decimal(str(raw_size)).quantize(q, rounding=ROUND_DOWN)
|
|
231
|
+
if d <= 0:
|
|
232
|
+
raise RuntimeError(f"size too small for {coin}; minimum unit is 1e-{sz_decimals}")
|
|
233
|
+
return float(d)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _resolve_tradable_coin(context: CLIContext, coin: str) -> str:
|
|
237
|
+
info = context.get_public_client()
|
|
238
|
+
target = coin.strip()
|
|
239
|
+
if not target:
|
|
240
|
+
raise RuntimeError("coin must not be empty")
|
|
241
|
+
|
|
242
|
+
mids = info.all_mids()
|
|
243
|
+
if target in mids:
|
|
244
|
+
return target
|
|
245
|
+
up = target.upper()
|
|
246
|
+
if up in mids:
|
|
247
|
+
return up
|
|
248
|
+
if ":" in target:
|
|
249
|
+
dex, sym = target.split(":", 1)
|
|
250
|
+
norm = f"{dex.lower()}:{sym.upper()}"
|
|
251
|
+
if norm in mids:
|
|
252
|
+
return norm
|
|
253
|
+
|
|
254
|
+
for m in info.meta().get("universe", []):
|
|
255
|
+
name = str(m.get("name", ""))
|
|
256
|
+
if name == target or name.upper() == up:
|
|
257
|
+
return name
|
|
258
|
+
|
|
259
|
+
perp_candidates: list[tuple[str, int]] = []
|
|
260
|
+
dex_names = [
|
|
261
|
+
str(dex_item.get("name", ""))
|
|
262
|
+
for dex_item in info.perp_dexs()
|
|
263
|
+
if isinstance(dex_item, dict) and dex_item.get("name")
|
|
264
|
+
]
|
|
265
|
+
builder_market_data = run_blocking(_fetch_all_builder_resolution_data(info, dex_names))
|
|
266
|
+
for meta, dex_mids in builder_market_data:
|
|
267
|
+
for m in meta.get("universe", []):
|
|
268
|
+
full_name = str(m.get("name", ""))
|
|
269
|
+
if not full_name:
|
|
270
|
+
continue
|
|
271
|
+
suffix = full_name.split(":", 1)[1] if ":" in full_name else full_name
|
|
272
|
+
if full_name.upper() == up or suffix.upper() == up:
|
|
273
|
+
if full_name not in dex_mids:
|
|
274
|
+
continue
|
|
275
|
+
lev = int(m.get("maxLeverage", 0) or 0)
|
|
276
|
+
perp_candidates.append((full_name, lev))
|
|
277
|
+
|
|
278
|
+
if perp_candidates:
|
|
279
|
+
perp_candidates.sort(key=lambda x: (x[1], x[0]), reverse=True)
|
|
280
|
+
return perp_candidates[0][0]
|
|
281
|
+
|
|
282
|
+
spot_meta = info.spot_meta()
|
|
283
|
+
tokens = spot_meta.get("tokens", [])
|
|
284
|
+
universe = spot_meta.get("universe", [])
|
|
285
|
+
usdc_index = next((t.get("index") for t in tokens if str(t.get("name", "")).upper() == "USDC"), 0)
|
|
286
|
+
token_index = next(
|
|
287
|
+
(
|
|
288
|
+
int(t.get("index"))
|
|
289
|
+
for t in tokens
|
|
290
|
+
if str(t.get("name", "")).upper() == up or str(t.get("fullName", "")).upper() == up
|
|
291
|
+
),
|
|
292
|
+
None,
|
|
293
|
+
)
|
|
294
|
+
if token_index is not None:
|
|
295
|
+
preferred = next(
|
|
296
|
+
(
|
|
297
|
+
str(p.get("name"))
|
|
298
|
+
for p in universe
|
|
299
|
+
if isinstance(p.get("tokens"), list)
|
|
300
|
+
and len(p["tokens"]) >= 2
|
|
301
|
+
and int(p["tokens"][0]) == token_index
|
|
302
|
+
and int(p["tokens"][1]) == int(usdc_index)
|
|
303
|
+
),
|
|
304
|
+
None,
|
|
305
|
+
)
|
|
306
|
+
if preferred and preferred in mids:
|
|
307
|
+
return preferred
|
|
308
|
+
fallback = next(
|
|
309
|
+
(
|
|
310
|
+
str(p.get("name"))
|
|
311
|
+
for p in universe
|
|
312
|
+
if isinstance(p.get("tokens"), list)
|
|
313
|
+
and token_index in [int(x) for x in p["tokens"]]
|
|
314
|
+
),
|
|
315
|
+
None,
|
|
316
|
+
)
|
|
317
|
+
if fallback and fallback in mids:
|
|
318
|
+
return fallback
|
|
319
|
+
|
|
320
|
+
raise RuntimeError(f"Coin not found: {coin}")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _mids_for_coin(context: CLIContext, coin: str) -> dict[str, str]:
|
|
324
|
+
info = context.get_public_client()
|
|
325
|
+
if ":" in coin:
|
|
326
|
+
dex = coin.split(":", 1)[0]
|
|
327
|
+
return info.all_mids(dex=dex)
|
|
328
|
+
return info.all_mids()
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _stake_to_position_notional(stake: float, leverage: Optional[int]) -> float:
|
|
332
|
+
if stake <= 0:
|
|
333
|
+
raise RuntimeError("stake must be a positive number")
|
|
334
|
+
lev = 1 if leverage is None else leverage
|
|
335
|
+
if lev <= 0:
|
|
336
|
+
raise RuntimeError("leverage must be a positive integer when used with --stake")
|
|
337
|
+
return stake * float(lev)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _place_native_twap(
|
|
341
|
+
*,
|
|
342
|
+
context: CLIContext,
|
|
343
|
+
coin: str,
|
|
344
|
+
is_buy: bool,
|
|
345
|
+
size: float,
|
|
346
|
+
minutes: int,
|
|
347
|
+
reduce_only: bool,
|
|
348
|
+
randomize: bool,
|
|
349
|
+
) -> dict[str, Any]:
|
|
350
|
+
exchange = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(coin))
|
|
351
|
+
asset = exchange.info.name_to_asset(coin)
|
|
352
|
+
action = {
|
|
353
|
+
"type": "twapOrder",
|
|
354
|
+
"twap": {
|
|
355
|
+
"a": asset,
|
|
356
|
+
"b": is_buy,
|
|
357
|
+
"s": float_to_wire(size),
|
|
358
|
+
"r": reduce_only,
|
|
359
|
+
"m": minutes,
|
|
360
|
+
"t": randomize,
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
nonce = get_timestamp_ms()
|
|
364
|
+
signature = sign_l1_action(
|
|
365
|
+
exchange.wallet,
|
|
366
|
+
action,
|
|
367
|
+
exchange.vault_address,
|
|
368
|
+
nonce,
|
|
369
|
+
exchange.expires_after,
|
|
370
|
+
exchange.base_url == MAINNET_API_URL,
|
|
371
|
+
)
|
|
372
|
+
return exchange._post_action(action, signature, nonce) # noqa: SLF001
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _cancel_native_twap(*, context: CLIContext, coin: str, twap_id: int) -> dict[str, Any]:
|
|
376
|
+
exchange = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(coin))
|
|
377
|
+
asset = exchange.info.name_to_asset(coin)
|
|
378
|
+
action = {"type": "twapCancel", "a": asset, "t": twap_id}
|
|
379
|
+
nonce = get_timestamp_ms()
|
|
380
|
+
signature = sign_l1_action(
|
|
381
|
+
exchange.wallet,
|
|
382
|
+
action,
|
|
383
|
+
exchange.vault_address,
|
|
384
|
+
nonce,
|
|
385
|
+
exchange.expires_after,
|
|
386
|
+
exchange.base_url == MAINNET_API_URL,
|
|
387
|
+
)
|
|
388
|
+
return exchange._post_action(action, signature, nonce) # noqa: SLF001
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _resolve_position_for_close(context: CLIContext, coin: str) -> tuple[str, float, bool]:
|
|
392
|
+
user = context.get_wallet_address()
|
|
393
|
+
info = context.get_public_client()
|
|
394
|
+
target = coin.strip()
|
|
395
|
+
if not target:
|
|
396
|
+
raise RuntimeError("coin must not be empty")
|
|
397
|
+
|
|
398
|
+
with_prefix = ":" in target
|
|
399
|
+
up = target.upper()
|
|
400
|
+
matches: list[tuple[str, float]] = []
|
|
401
|
+
states = run_blocking(_fetch_all_perp_states(info, user, context.get_perp_dexs()))
|
|
402
|
+
for state in states:
|
|
403
|
+
for row in state.get("assetPositions", []):
|
|
404
|
+
pos = row.get("position", {})
|
|
405
|
+
pos_coin = str(pos.get("coin", ""))
|
|
406
|
+
if not pos_coin:
|
|
407
|
+
continue
|
|
408
|
+
szi = float(pos.get("szi", 0) or 0)
|
|
409
|
+
if szi == 0:
|
|
410
|
+
continue
|
|
411
|
+
suffix = pos_coin.split(":", 1)[1] if ":" in pos_coin else pos_coin
|
|
412
|
+
if with_prefix:
|
|
413
|
+
if pos_coin.lower() == target.lower():
|
|
414
|
+
matches.append((pos_coin, szi))
|
|
415
|
+
elif pos_coin.upper() == up or suffix.upper() == up:
|
|
416
|
+
matches.append((pos_coin, szi))
|
|
417
|
+
|
|
418
|
+
if not matches:
|
|
419
|
+
raise RuntimeError(f"No open position found for {coin}")
|
|
420
|
+
if not with_prefix and len(matches) > 1:
|
|
421
|
+
coins = ", ".join(sorted({m[0] for m in matches}))
|
|
422
|
+
raise RuntimeError(
|
|
423
|
+
f"Multiple open positions matched '{coin}': {coins}. "
|
|
424
|
+
"Please specify the dex-prefixed symbol (e.g. xyz:TSLA)."
|
|
425
|
+
)
|
|
426
|
+
resolved_coin, szi = matches[0]
|
|
427
|
+
return resolved_coin, abs(szi), (szi < 0)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _fetch_builder_resolution_data(info: Any, dex_name: str) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
431
|
+
return info.meta(dex=dex_name), info.all_mids(dex=dex_name)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
async def _fetch_all_builder_resolution_data(
|
|
435
|
+
info: Any,
|
|
436
|
+
dex_names: list[str],
|
|
437
|
+
) -> list[tuple[dict[str, Any], dict[str, Any]]]:
|
|
438
|
+
return await asyncio.gather(
|
|
439
|
+
*(asyncio.to_thread(_fetch_builder_resolution_data, info, dex_name) for dex_name in dex_names)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
async def _fetch_all_perp_states(info: Any, user: str, dexs: list[str]) -> list[dict[str, Any]]:
|
|
444
|
+
return await asyncio.gather(
|
|
445
|
+
*(asyncio.to_thread(info.user_state, user, dex) for dex in dexs)
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _fetch_orders(context: CLIContext, user: str) -> list[dict[str, Any]]:
|
|
450
|
+
orders = context.get_public_client().open_orders(user)
|
|
451
|
+
return [
|
|
452
|
+
{
|
|
453
|
+
"oid": o["oid"],
|
|
454
|
+
"coin": o["coin"],
|
|
455
|
+
"side": "Buy" if o["side"] in {"B", "buy", True} else "Sell",
|
|
456
|
+
"sz": o["sz"],
|
|
457
|
+
"limitPx": o["limitPx"],
|
|
458
|
+
"timestamp": datetime.fromtimestamp(o["timestamp"] / 1000).isoformat(),
|
|
459
|
+
}
|
|
460
|
+
for o in orders
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@cli_command
|
|
465
|
+
def order_ls(
|
|
466
|
+
ctx: Any,
|
|
467
|
+
user: Optional[str] = None,
|
|
468
|
+
watch: bool = False,
|
|
469
|
+
) -> None:
|
|
470
|
+
context = _ctx(ctx)
|
|
471
|
+
address = user if user else context.get_wallet_address()
|
|
472
|
+
if watch:
|
|
473
|
+
watch_loop(
|
|
474
|
+
lambda: _fetch_orders(context, address),
|
|
475
|
+
lambda rows: _render_table(
|
|
476
|
+
"Open Orders",
|
|
477
|
+
["OID", "Coin", "Side", "Size", "Price", "Time"],
|
|
478
|
+
[[r["oid"], r["coin"], r["side"], r["sz"], r["limitPx"], r["timestamp"]] for r in rows],
|
|
479
|
+
),
|
|
480
|
+
as_json=_json(ctx),
|
|
481
|
+
)
|
|
482
|
+
return
|
|
483
|
+
out(_fetch_orders(context, address), _json(ctx))
|
|
484
|
+
_done(ctx)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
@cli_command
|
|
488
|
+
def order_limit(
|
|
489
|
+
ctx: Any,
|
|
490
|
+
side: str,
|
|
491
|
+
size: str,
|
|
492
|
+
coin: str,
|
|
493
|
+
price: str,
|
|
494
|
+
tif: str = "Gtc",
|
|
495
|
+
reduce_only: bool = False,
|
|
496
|
+
stake: Optional[float] = None,
|
|
497
|
+
leverage: Optional[int] = None,
|
|
498
|
+
cross: bool = False,
|
|
499
|
+
isolated: bool = False,
|
|
500
|
+
) -> None:
|
|
501
|
+
context = _ctx(ctx)
|
|
502
|
+
resolved_coin = _resolve_tradable_coin(context, coin)
|
|
503
|
+
client = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(resolved_coin))
|
|
504
|
+
is_buy = normalize_side(side) == "buy"
|
|
505
|
+
limit_price = validate_positive_number(price, "price")
|
|
506
|
+
if stake is not None:
|
|
507
|
+
position_notional = _stake_to_position_notional(stake, leverage)
|
|
508
|
+
order_size = position_notional / limit_price
|
|
509
|
+
else:
|
|
510
|
+
order_size = validate_positive_number(size, "size")
|
|
511
|
+
order_size = _normalize_size_for_coin(context, resolved_coin, order_size)
|
|
512
|
+
lev_result = _maybe_update_leverage(
|
|
513
|
+
context=context,
|
|
514
|
+
coin=resolved_coin,
|
|
515
|
+
leverage=leverage,
|
|
516
|
+
cross=cross,
|
|
517
|
+
isolated=isolated,
|
|
518
|
+
emit_warning=not _json(ctx),
|
|
519
|
+
)
|
|
520
|
+
result = client.order(
|
|
521
|
+
resolved_coin,
|
|
522
|
+
is_buy,
|
|
523
|
+
order_size,
|
|
524
|
+
limit_price,
|
|
525
|
+
{"limit": {"tif": normalize_tif(tif)}},
|
|
526
|
+
reduce_only=reduce_only,
|
|
527
|
+
)
|
|
528
|
+
if _json(ctx):
|
|
529
|
+
out({"leverageUpdate": lev_result, "order": result} if lev_result is not None else result, True)
|
|
530
|
+
else:
|
|
531
|
+
_print_leverage_update(lev_result, coin, leverage, cross or not isolated)
|
|
532
|
+
_print_order_feedback(
|
|
533
|
+
result=result,
|
|
534
|
+
coin=coin,
|
|
535
|
+
side="buy" if is_buy else "sell",
|
|
536
|
+
order_kind="Limit",
|
|
537
|
+
stake=stake,
|
|
538
|
+
)
|
|
539
|
+
_done(ctx)
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@cli_command
|
|
543
|
+
def order_market(
|
|
544
|
+
ctx: Any,
|
|
545
|
+
side: str,
|
|
546
|
+
size: str,
|
|
547
|
+
coin: str,
|
|
548
|
+
reduce_only: bool = False,
|
|
549
|
+
slippage: Optional[float] = None,
|
|
550
|
+
stake: Optional[float] = None,
|
|
551
|
+
leverage: Optional[int] = None,
|
|
552
|
+
cross: bool = False,
|
|
553
|
+
isolated: bool = False,
|
|
554
|
+
) -> None:
|
|
555
|
+
context = _ctx(ctx)
|
|
556
|
+
resolved_coin = _resolve_tradable_coin(context, coin)
|
|
557
|
+
client = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(resolved_coin))
|
|
558
|
+
is_buy = normalize_side(side) == "buy"
|
|
559
|
+
cfg = get_order_config()
|
|
560
|
+
slippage_pct = (slippage if slippage is not None else float(cfg["slippage"])) / 100
|
|
561
|
+
mids_cache: Optional[dict[str, str]] = None
|
|
562
|
+
if stake is not None:
|
|
563
|
+
mids_cache = _mids_for_coin(context, resolved_coin)
|
|
564
|
+
mid = float(mids_cache[resolved_coin])
|
|
565
|
+
position_notional = _stake_to_position_notional(stake, leverage)
|
|
566
|
+
order_size = position_notional / mid
|
|
567
|
+
else:
|
|
568
|
+
order_size = validate_positive_number(size, "size")
|
|
569
|
+
order_size = _normalize_size_for_coin(context, resolved_coin, order_size)
|
|
570
|
+
lev_result = _maybe_update_leverage(
|
|
571
|
+
context=context,
|
|
572
|
+
coin=resolved_coin,
|
|
573
|
+
leverage=leverage,
|
|
574
|
+
cross=cross,
|
|
575
|
+
isolated=isolated,
|
|
576
|
+
emit_warning=not _json(ctx),
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
if reduce_only:
|
|
580
|
+
mids = mids_cache if mids_cache is not None else _mids_for_coin(context, resolved_coin)
|
|
581
|
+
mid = float(mids[resolved_coin])
|
|
582
|
+
price = mid * (1 + slippage_pct) if is_buy else mid * (1 - slippage_pct)
|
|
583
|
+
result = client.order(
|
|
584
|
+
resolved_coin,
|
|
585
|
+
is_buy,
|
|
586
|
+
order_size,
|
|
587
|
+
price,
|
|
588
|
+
{"limit": {"tif": "Ioc"}},
|
|
589
|
+
reduce_only=True,
|
|
590
|
+
)
|
|
591
|
+
else:
|
|
592
|
+
result = client.market_open(
|
|
593
|
+
resolved_coin,
|
|
594
|
+
is_buy,
|
|
595
|
+
order_size,
|
|
596
|
+
slippage=slippage_pct,
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
if _json(ctx):
|
|
600
|
+
out({"leverageUpdate": lev_result, "order": result} if lev_result is not None else result, True)
|
|
601
|
+
else:
|
|
602
|
+
_print_leverage_update(lev_result, coin, leverage, cross or not isolated)
|
|
603
|
+
_print_order_feedback(
|
|
604
|
+
result=result,
|
|
605
|
+
coin=coin,
|
|
606
|
+
side="buy" if is_buy else "sell",
|
|
607
|
+
order_kind="Market",
|
|
608
|
+
stake=stake,
|
|
609
|
+
)
|
|
610
|
+
_done(ctx)
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@cli_command
|
|
614
|
+
def order_market_close(
|
|
615
|
+
ctx: Any,
|
|
616
|
+
coin: str,
|
|
617
|
+
slippage: Optional[float] = None,
|
|
618
|
+
ratio: float = 1.0,
|
|
619
|
+
) -> None:
|
|
620
|
+
if ratio <= 0 or ratio > 1:
|
|
621
|
+
raise RuntimeError("ratio must be > 0 and <= 1")
|
|
622
|
+
context = _ctx(ctx)
|
|
623
|
+
resolved_coin, order_size, is_buy = _resolve_position_for_close(context, coin)
|
|
624
|
+
client = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(resolved_coin))
|
|
625
|
+
close_size = order_size * ratio
|
|
626
|
+
cfg = get_order_config()
|
|
627
|
+
slippage_pct = (slippage if slippage is not None else float(cfg["slippage"])) / 100
|
|
628
|
+
result = client.market_close(
|
|
629
|
+
resolved_coin,
|
|
630
|
+
sz=_normalize_size_for_coin(context, resolved_coin, close_size),
|
|
631
|
+
slippage=slippage_pct,
|
|
632
|
+
)
|
|
633
|
+
if _json(ctx):
|
|
634
|
+
out(result, True)
|
|
635
|
+
else:
|
|
636
|
+
_print_order_feedback(
|
|
637
|
+
result=result,
|
|
638
|
+
coin=coin,
|
|
639
|
+
side="buy" if is_buy else "sell",
|
|
640
|
+
order_kind="Market close",
|
|
641
|
+
stake=None,
|
|
642
|
+
)
|
|
643
|
+
_done(ctx)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
@cli_command
|
|
647
|
+
def order_tpsl(
|
|
648
|
+
ctx: Any,
|
|
649
|
+
coin: str,
|
|
650
|
+
tp: Optional[float] = None,
|
|
651
|
+
sl: Optional[float] = None,
|
|
652
|
+
ratio: float = 1.0,
|
|
653
|
+
) -> None:
|
|
654
|
+
if tp is None and sl is None:
|
|
655
|
+
raise RuntimeError("Specify at least one of --tp or --sl")
|
|
656
|
+
if tp is not None and tp <= 0:
|
|
657
|
+
raise RuntimeError("tp must be a positive number")
|
|
658
|
+
if sl is not None and sl <= 0:
|
|
659
|
+
raise RuntimeError("sl must be a positive number")
|
|
660
|
+
if ratio <= 0 or ratio > 1:
|
|
661
|
+
raise RuntimeError("ratio must be > 0 and <= 1")
|
|
662
|
+
|
|
663
|
+
context = _ctx(ctx)
|
|
664
|
+
resolved_coin, position_size, is_buy_to_close = _resolve_position_for_close(context, coin)
|
|
665
|
+
client = context.get_wallet_client(perp_dexs=_wallet_perp_dexs_for_coin(resolved_coin))
|
|
666
|
+
protected_size = _normalize_size_for_coin(context, resolved_coin, position_size * ratio)
|
|
667
|
+
|
|
668
|
+
results: dict[str, Any] = {
|
|
669
|
+
"coin": coin,
|
|
670
|
+
"resolvedCoin": resolved_coin,
|
|
671
|
+
"closeSide": "buy" if is_buy_to_close else "sell",
|
|
672
|
+
"size": protected_size,
|
|
673
|
+
"ratio": ratio,
|
|
674
|
+
}
|
|
675
|
+
if tp is not None:
|
|
676
|
+
tp_order_type = {"trigger": {"triggerPx": tp, "isMarket": True, "tpsl": "tp"}}
|
|
677
|
+
results["tp"] = client.order(
|
|
678
|
+
resolved_coin,
|
|
679
|
+
is_buy_to_close,
|
|
680
|
+
protected_size,
|
|
681
|
+
tp,
|
|
682
|
+
tp_order_type,
|
|
683
|
+
reduce_only=True,
|
|
684
|
+
)
|
|
685
|
+
if sl is not None:
|
|
686
|
+
sl_order_type = {"trigger": {"triggerPx": sl, "isMarket": True, "tpsl": "sl"}}
|
|
687
|
+
results["sl"] = client.order(
|
|
688
|
+
resolved_coin,
|
|
689
|
+
is_buy_to_close,
|
|
690
|
+
protected_size,
|
|
691
|
+
sl,
|
|
692
|
+
sl_order_type,
|
|
693
|
+
reduce_only=True,
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
if _json(ctx):
|
|
697
|
+
out({"tpsl": results}, True)
|
|
698
|
+
else:
|
|
699
|
+
print("✅ TP/SL orders submitted")
|
|
700
|
+
print(f"\nAsset: {coin}")
|
|
701
|
+
print(f"Close side: {'BUY' if is_buy_to_close else 'SELL'}")
|
|
702
|
+
print(f"Protected size: {protected_size}")
|
|
703
|
+
if tp is not None:
|
|
704
|
+
print(f"TP trigger: {tp}")
|
|
705
|
+
if sl is not None:
|
|
706
|
+
print(f"SL trigger: {sl}")
|
|
707
|
+
_done(ctx)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
@cli_command
|
|
711
|
+
def order_twap(
|
|
712
|
+
ctx: Any,
|
|
713
|
+
side: str,
|
|
714
|
+
size: str,
|
|
715
|
+
coin: str,
|
|
716
|
+
interval: str,
|
|
717
|
+
stake: Optional[float] = None,
|
|
718
|
+
reduce_only: bool = False,
|
|
719
|
+
randomize: bool = False,
|
|
720
|
+
leverage: Optional[int] = None,
|
|
721
|
+
cross: bool = False,
|
|
722
|
+
isolated: bool = False,
|
|
723
|
+
) -> None:
|
|
724
|
+
context = _ctx(ctx)
|
|
725
|
+
resolved_coin = _resolve_tradable_coin(context, coin)
|
|
726
|
+
is_buy = normalize_side(side) == "buy"
|
|
727
|
+
if stake is not None:
|
|
728
|
+
mids = _mids_for_coin(context, resolved_coin)
|
|
729
|
+
mid = float(mids[resolved_coin])
|
|
730
|
+
position_notional = _stake_to_position_notional(stake, leverage)
|
|
731
|
+
total_size = position_notional / mid
|
|
732
|
+
else:
|
|
733
|
+
total_size = validate_positive_number(size, "size")
|
|
734
|
+
total_size = _normalize_size_for_coin(context, resolved_coin, total_size)
|
|
735
|
+
minutes, compatibility_orders = _parse_twap_interval(interval)
|
|
736
|
+
lev_result = _maybe_update_leverage(
|
|
737
|
+
context=context,
|
|
738
|
+
coin=resolved_coin,
|
|
739
|
+
leverage=leverage,
|
|
740
|
+
cross=cross,
|
|
741
|
+
isolated=isolated,
|
|
742
|
+
emit_warning=not _json(ctx),
|
|
743
|
+
)
|
|
744
|
+
response = _place_native_twap(
|
|
745
|
+
context=context,
|
|
746
|
+
coin=resolved_coin,
|
|
747
|
+
is_buy=is_buy,
|
|
748
|
+
size=total_size,
|
|
749
|
+
minutes=minutes,
|
|
750
|
+
reduce_only=reduce_only,
|
|
751
|
+
randomize=randomize,
|
|
752
|
+
)
|
|
753
|
+
result = {
|
|
754
|
+
"twap": {
|
|
755
|
+
"side": "buy" if is_buy else "sell",
|
|
756
|
+
"coin": coin,
|
|
757
|
+
"totalSize": total_size,
|
|
758
|
+
"stake": stake,
|
|
759
|
+
"durationMinutes": minutes,
|
|
760
|
+
"compatibilityInput": interval if compatibility_orders > 1 else None,
|
|
761
|
+
"randomize": randomize,
|
|
762
|
+
"reduceOnly": reduce_only,
|
|
763
|
+
"leverageUpdate": lev_result,
|
|
764
|
+
"response": response,
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if _json(ctx):
|
|
768
|
+
out(result, True)
|
|
769
|
+
else:
|
|
770
|
+
_print_leverage_update(lev_result, coin, leverage, cross or not isolated)
|
|
771
|
+
status = response.get("response", {}).get("data", {}).get("status", {})
|
|
772
|
+
if isinstance(status, dict) and "error" in status:
|
|
773
|
+
print("❌ TWAP order rejected")
|
|
774
|
+
print(f"\nReason: {status.get('error')}")
|
|
775
|
+
else:
|
|
776
|
+
print("✅ TWAP order submitted")
|
|
777
|
+
print(f"\nAsset: {coin}")
|
|
778
|
+
print(f"Side: {'BUY' if is_buy else 'SELL'}")
|
|
779
|
+
print(f"Total size: {total_size} {coin}")
|
|
780
|
+
print(f"Duration: {minutes} min")
|
|
781
|
+
print(f"Randomize: {'on' if randomize else 'off'}")
|
|
782
|
+
_done(ctx)
|
|
783
|
+
|
|
784
|
+
|
|
785
|
+
@cli_command
|
|
786
|
+
def order_twap_cancel(ctx: Any, coin: str, twap_id: str) -> None:
|
|
787
|
+
context = _ctx(ctx)
|
|
788
|
+
twap_num = validate_positive_integer(twap_id, "twap_id")
|
|
789
|
+
response = _cancel_native_twap(context=context, coin=coin, twap_id=twap_num)
|
|
790
|
+
out({"twapCancel": {"coin": coin, "twapId": twap_num, "response": response}}, _json(ctx))
|
|
791
|
+
_done(ctx)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
@cli_command
|
|
795
|
+
def order_cancel(ctx: Any, oid: Optional[str] = None) -> None:
|
|
796
|
+
context = _ctx(ctx)
|
|
797
|
+
user = context.get_wallet_address()
|
|
798
|
+
exchange = context.get_wallet_client()
|
|
799
|
+
orders = context.get_public_client().open_orders(user)
|
|
800
|
+
|
|
801
|
+
if not orders:
|
|
802
|
+
if _json(ctx):
|
|
803
|
+
out(
|
|
804
|
+
{
|
|
805
|
+
"cancelled": False,
|
|
806
|
+
"reason": "no_open_orders",
|
|
807
|
+
"message": "No open orders to cancel",
|
|
808
|
+
},
|
|
809
|
+
True,
|
|
810
|
+
)
|
|
811
|
+
else:
|
|
812
|
+
out_success("No open orders to cancel")
|
|
813
|
+
_done(ctx)
|
|
814
|
+
return
|
|
815
|
+
|
|
816
|
+
if oid is None:
|
|
817
|
+
if _json(ctx):
|
|
818
|
+
latest = max(orders, key=lambda x: int(x.get("timestamp", 0)))
|
|
819
|
+
oid = str(latest["oid"])
|
|
820
|
+
else:
|
|
821
|
+
_render_table(
|
|
822
|
+
"Open Orders",
|
|
823
|
+
["OID", "Coin", "Side", "Size", "Price"],
|
|
824
|
+
[[o["oid"], o["coin"], o["side"], o["sz"], o["limitPx"]] for o in orders],
|
|
825
|
+
)
|
|
826
|
+
oid = input("Select OID to cancel: ").strip()
|
|
827
|
+
|
|
828
|
+
order_id = validate_positive_integer(oid, "oid")
|
|
829
|
+
target = next((o for o in orders if int(o["oid"]) == order_id), None)
|
|
830
|
+
if not target:
|
|
831
|
+
raise RuntimeError(f"Order {order_id} not found")
|
|
832
|
+
|
|
833
|
+
result = exchange.cancel(target["coin"], order_id)
|
|
834
|
+
out(result, _json(ctx))
|
|
835
|
+
_done(ctx)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
@cli_command
|
|
839
|
+
def order_cancel_all(
|
|
840
|
+
ctx: Any,
|
|
841
|
+
yes: bool = False,
|
|
842
|
+
coin: Optional[str] = None,
|
|
843
|
+
) -> None:
|
|
844
|
+
context = _ctx(ctx)
|
|
845
|
+
user = context.get_wallet_address()
|
|
846
|
+
exchange = context.get_wallet_client()
|
|
847
|
+
orders = context.get_public_client().open_orders(user)
|
|
848
|
+
if coin:
|
|
849
|
+
orders = [o for o in orders if o["coin"] == coin]
|
|
850
|
+
if not orders:
|
|
851
|
+
if _json(ctx):
|
|
852
|
+
out(
|
|
853
|
+
{
|
|
854
|
+
"cancelled": 0,
|
|
855
|
+
"reason": "no_open_orders",
|
|
856
|
+
"message": "No open orders to cancel",
|
|
857
|
+
},
|
|
858
|
+
True,
|
|
859
|
+
)
|
|
860
|
+
else:
|
|
861
|
+
out_success("No open orders to cancel")
|
|
862
|
+
_done(ctx)
|
|
863
|
+
return
|
|
864
|
+
if not yes and not _confirm(f"Cancel {len(orders)} orders?", False):
|
|
865
|
+
if _json(ctx):
|
|
866
|
+
out({"cancelled": 0, "reason": "user_cancelled", "message": "Cancelled"}, True)
|
|
867
|
+
else:
|
|
868
|
+
out_success("Cancelled")
|
|
869
|
+
_done(ctx)
|
|
870
|
+
return
|
|
871
|
+
result = exchange.bulk_cancel([{"coin": o["coin"], "oid": int(o["oid"])} for o in orders])
|
|
872
|
+
out(result, _json(ctx))
|
|
873
|
+
_done(ctx)
|
|
874
|
+
|
|
875
|
+
|
|
876
|
+
@cli_command
|
|
877
|
+
def order_set_leverage(
|
|
878
|
+
ctx: Any,
|
|
879
|
+
coin: str,
|
|
880
|
+
leverage: str,
|
|
881
|
+
cross: bool = False,
|
|
882
|
+
isolated: bool = False,
|
|
883
|
+
) -> None:
|
|
884
|
+
context = _ctx(ctx)
|
|
885
|
+
if cross and isolated:
|
|
886
|
+
raise RuntimeError("Use only one of --cross or --isolated")
|
|
887
|
+
is_cross = cross or not isolated
|
|
888
|
+
requested = validate_positive_integer(leverage, "leverage")
|
|
889
|
+
result = _update_leverage_with_fallback(
|
|
890
|
+
context=context,
|
|
891
|
+
coin=coin,
|
|
892
|
+
leverage=requested,
|
|
893
|
+
is_cross=is_cross,
|
|
894
|
+
emit_warning=not _json(ctx),
|
|
895
|
+
)
|
|
896
|
+
if _json(ctx):
|
|
897
|
+
out({"requestedLeverage": requested, "result": result}, True)
|
|
898
|
+
else:
|
|
899
|
+
if result.get("status") == "ok":
|
|
900
|
+
print("✅ Leverage updated")
|
|
901
|
+
print(f"\nAsset: {coin}")
|
|
902
|
+
print(f"Leverage: {requested}x")
|
|
903
|
+
print(f"Margin type: {'cross' if is_cross else 'isolated'}")
|
|
904
|
+
else:
|
|
905
|
+
print("❌ Leverage update failed")
|
|
906
|
+
print(f"\nReason: {result.get('response')}")
|
|
907
|
+
_done(ctx)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
@cli_command
|
|
911
|
+
def order_configure(ctx: Any, slippage: Optional[float] = None) -> None:
|
|
912
|
+
if slippage is None:
|
|
913
|
+
out(get_order_config(), _json(ctx))
|
|
914
|
+
else:
|
|
915
|
+
if slippage < 0:
|
|
916
|
+
raise RuntimeError("Slippage must be a non-negative number")
|
|
917
|
+
out(update_order_config(slippage=slippage), _json(ctx))
|
|
918
|
+
_done(ctx)
|