perp-cli 0.4.11 → 0.5.0

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.
@@ -8,47 +8,55 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
8
8
  market
9
9
  .command("list")
10
10
  .description("List all available markets")
11
- .action(async () => {
11
+ .option("--hip3", "Include HIP-3 dex markets (Hyperliquid)")
12
+ .action(async (opts) => {
12
13
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
14
+ const HEADERS = ["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"];
15
+ const marketRow = (m) => [
16
+ chalk.white.bold(m.symbol),
17
+ `$${formatUsd(m.markPrice)}`,
18
+ `$${formatUsd(m.indexPrice)}`,
19
+ formatPercent(m.fundingRate),
20
+ `$${formatUsd(m.volume24h)}`,
21
+ `$${formatUsd(m.openInterest)}`,
22
+ String(m.maxLeverage) + "x",
23
+ ];
13
24
  // Multi-exchange mode
14
25
  if (!explicitExchange && getAdapterForExchange) {
15
- const allRows = [];
26
+ const grouped = {};
16
27
  const errors = {};
17
28
  await Promise.all(EXCHANGES.map(async (ex) => {
18
29
  try {
19
30
  const adapter = await getAdapterForExchange(ex);
20
- const markets = await adapter.getMarkets();
21
- for (const m of markets) {
22
- allRows.push([
23
- chalk.white.bold(m.symbol),
24
- chalk.gray(ex.slice(0, 3).toUpperCase()),
25
- `$${formatUsd(m.markPrice)}`,
26
- `$${formatUsd(m.indexPrice)}`,
27
- formatPercent(m.fundingRate),
28
- `$${formatUsd(m.volume24h)}`,
29
- `$${formatUsd(m.openInterest)}`,
30
- String(m.maxLeverage) + "x",
31
- ]);
31
+ grouped[ex] = await adapter.getMarkets();
32
+ // --hip3: fetch HIP-3 dex markets
33
+ if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
34
+ const dexes = await adapter.listDeployedDexes();
35
+ await Promise.allSettled(dexes.map(async (d) => {
36
+ const dexAdapter = Object.create(adapter);
37
+ dexAdapter.setDex(d.name);
38
+ const markets = await dexAdapter.getMarkets();
39
+ if (markets.length > 0)
40
+ grouped[`hip3:${d.name}`] = markets;
41
+ }));
32
42
  }
33
43
  }
34
44
  catch (err) {
35
45
  errors[ex] = err instanceof Error ? err.message : String(err);
36
46
  }
37
47
  }));
38
- // Sort by symbol name
39
- allRows.sort((a, b) => a[0].localeCompare(b[0]));
40
48
  if (isJson()) {
41
- const grouped = {};
42
- await Promise.all(EXCHANGES.map(async (ex) => {
43
- try {
44
- const adapter = await getAdapterForExchange(ex);
45
- grouped[ex] = await adapter.getMarkets();
46
- }
47
- catch { /* skip */ }
48
- }));
49
49
  return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
50
50
  }
51
- console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], allRows));
51
+ // Per-exchange tables
52
+ for (const [ex, markets] of Object.entries(grouped)) {
53
+ if (markets.length === 0)
54
+ continue;
55
+ const isHip3 = ex.startsWith("hip3:");
56
+ const label = isHip3 ? chalk.magenta.bold(` HIP-3: ${ex.slice(5)}`) : chalk.cyan.bold(` ${ex.toUpperCase()}`);
57
+ console.log(`\n${label} ${chalk.gray(`(${markets.length} markets)`)}`);
58
+ console.log(makeTable(HEADERS, markets.map(marketRow)));
59
+ }
52
60
  for (const [ex, msg] of Object.entries(errors)) {
53
61
  console.log(chalk.gray(` ${ex}: ${msg}`));
54
62
  }
@@ -57,58 +65,63 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
57
65
  // Single exchange mode
58
66
  const adapter = await getAdapter();
59
67
  const markets = await adapter.getMarkets();
60
- if (isJson())
68
+ // --hip3: fetch HIP-3 dex markets
69
+ const hip3 = {};
70
+ if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
71
+ const dexes = await adapter.listDeployedDexes();
72
+ await Promise.allSettled(dexes.map(async (d) => {
73
+ const dexAdapter = Object.create(adapter);
74
+ dexAdapter.setDex(d.name);
75
+ const m = await dexAdapter.getMarkets();
76
+ if (m.length > 0)
77
+ hip3[d.name] = m;
78
+ }));
79
+ }
80
+ if (isJson()) {
81
+ if (Object.keys(hip3).length > 0) {
82
+ return printJson(jsonOk({ main: markets, hip3 }));
83
+ }
61
84
  return printJson(jsonOk(markets));
62
- const rows = markets.map((m) => [
63
- chalk.white.bold(m.symbol),
64
- `$${formatUsd(m.markPrice)}`,
65
- `$${formatUsd(m.indexPrice)}`,
66
- formatPercent(m.fundingRate),
67
- `$${formatUsd(m.volume24h)}`,
68
- `$${formatUsd(m.openInterest)}`,
69
- String(m.maxLeverage) + "x",
70
- ]);
71
- console.log(makeTable(["Symbol", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], rows));
85
+ }
86
+ console.log(makeTable(HEADERS, markets.map(marketRow)));
87
+ for (const [dex, m] of Object.entries(hip3)) {
88
+ console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`) + chalk.gray(` (${m.length} markets)`));
89
+ console.log(makeTable(HEADERS, m.map(marketRow)));
90
+ }
72
91
  });
73
92
  market
74
93
  .command("prices")
75
94
  .description("Get current prices for all markets")
76
95
  .action(async () => {
77
96
  const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
97
+ const PRICE_HEADERS = ["Symbol", "Mark", "Index", "Funding"];
98
+ const priceRow = (m) => [
99
+ chalk.white.bold(m.symbol),
100
+ `$${formatUsd(m.markPrice)}`,
101
+ `$${formatUsd(m.indexPrice)}`,
102
+ formatPercent(m.fundingRate),
103
+ ];
78
104
  if (!explicitExchange && getAdapterForExchange) {
79
- const allRows = [];
105
+ const grouped = {};
80
106
  const errors = {};
81
107
  await Promise.all(EXCHANGES.map(async (ex) => {
82
108
  try {
83
109
  const adapter = await getAdapterForExchange(ex);
84
- const markets = await adapter.getMarkets();
85
- for (const m of markets) {
86
- allRows.push([
87
- chalk.white.bold(m.symbol),
88
- chalk.gray(ex.slice(0, 3).toUpperCase()),
89
- `$${formatUsd(m.markPrice)}`,
90
- `$${formatUsd(m.indexPrice)}`,
91
- formatPercent(m.fundingRate),
92
- ]);
93
- }
110
+ grouped[ex] = await adapter.getMarkets();
94
111
  }
95
112
  catch (err) {
96
113
  errors[ex] = err instanceof Error ? err.message : String(err);
97
114
  }
98
115
  }));
99
- allRows.sort((a, b) => a[0].localeCompare(b[0]));
100
116
  if (isJson()) {
101
- const grouped = {};
102
- await Promise.all(EXCHANGES.map(async (ex) => {
103
- try {
104
- const adapter = await getAdapterForExchange(ex);
105
- grouped[ex] = await adapter.getMarkets();
106
- }
107
- catch { /* skip */ }
108
- }));
109
117
  return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
110
118
  }
111
- console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding"], allRows));
119
+ for (const [ex, markets] of Object.entries(grouped)) {
120
+ if (markets.length === 0)
121
+ continue;
122
+ console.log(chalk.cyan.bold(`\n ${ex.toUpperCase()}`) + chalk.gray(` (${markets.length} markets)`));
123
+ console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
124
+ }
112
125
  for (const [ex, msg] of Object.entries(errors))
113
126
  console.log(chalk.gray(` ${ex}: ${msg}`));
114
127
  return;
@@ -128,17 +141,10 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
128
141
  console.log(makeTable(["Symbol", "Mark", "Mid", "Oracle", "Funding"], rows));
129
142
  }
130
143
  else {
131
- // Generic: use getMarkets as price source
132
144
  const markets = await adapter.getMarkets();
133
145
  if (isJson())
134
146
  return printJson(jsonOk(markets));
135
- const rows = markets.map((m) => [
136
- chalk.white.bold(m.symbol),
137
- `$${formatUsd(m.markPrice)}`,
138
- `$${formatUsd(m.indexPrice)}`,
139
- formatPercent(m.fundingRate),
140
- ]);
141
- console.log(makeTable(["Symbol", "Mark", "Index", "Funding"], rows));
147
+ console.log(makeTable(PRICE_HEADERS, markets.map(priceRow)));
142
148
  }
143
149
  });
144
150
  market
@@ -148,7 +154,11 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
148
154
  const adapter = await getAdapter();
149
155
  const markets = await adapter.getMarkets();
150
156
  const sym = symbol.toUpperCase();
151
- const m = markets.find((mk) => mk.symbol === sym || mk.symbol === `${sym}-PERP` || mk.symbol.replace(/-PERP$/, "") === sym);
157
+ const m = markets.find((mk) => {
158
+ const s = mk.symbol.toUpperCase();
159
+ return s === sym || s === `${sym}-PERP` || s.replace(/-PERP$/, "") === sym
160
+ || s.split(":").pop() === sym; // HIP-3: "km:US500" matches "US500"
161
+ });
152
162
  if (!m) {
153
163
  if (isJson())
154
164
  return printJson(jsonError("SYMBOL_NOT_FOUND", `Market ${sym} not found`));
@@ -178,15 +188,40 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
178
188
  const depth = parseInt(opts.depth);
179
189
  const asks = book.asks.slice(0, depth).reverse();
180
190
  const bids = book.bids.slice(0, depth);
191
+ // Compute spread
192
+ const bestBid = bids[0] ? Number(bids[0][0]) : 0;
193
+ const bestAsk = asks[asks.length - 1] ? Number(asks[asks.length - 1][0]) : 0;
194
+ const midPrice = bestBid && bestAsk ? (bestBid + bestAsk) / 2 : bestBid || bestAsk;
195
+ const spreadAbs = bestBid && bestAsk ? bestAsk - bestBid : 0;
196
+ const spreadPct = midPrice > 0 ? (spreadAbs / midPrice) * 100 : 0;
197
+ // Compute cumulative sizes + max for bar scaling
198
+ let askCum = 0;
199
+ const askCums = asks.map(([, a]) => { askCum += Number(a); return askCum; });
200
+ let bidCum = 0;
201
+ const bidCums = bids.map(([, a]) => { bidCum += Number(a); return bidCum; });
202
+ const maxCum = Math.max(askCums[askCums.length - 1] || 0, bidCums[bidCums.length - 1] || 0);
203
+ const BAR_WIDTH = 20;
204
+ const bar = (cum) => {
205
+ const len = maxCum > 0 ? Math.round((cum / maxCum) * BAR_WIDTH) : 0;
206
+ return "█".repeat(len);
207
+ };
181
208
  console.log(chalk.cyan.bold(`\n Orderbook: ${symbol.toUpperCase()} (${adapter.name})\n`));
182
- console.log(chalk.gray(" Price Size"));
183
- console.log(chalk.gray(" ─────────────────────"));
184
- asks.forEach(([p, a]) => {
185
- console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${a}`));
209
+ console.log(chalk.gray(" Price Size Cum$ Depth"));
210
+ console.log(chalk.gray(" " + "─".repeat(60)));
211
+ asks.forEach(([p, a], i) => {
212
+ const cumUsd = askCums[i] * (Number(p));
213
+ console.log(chalk.red(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
214
+ chalk.red.dim(bar(askCums[i])));
186
215
  });
187
- console.log(chalk.gray(" ─── spread ───"));
188
- bids.forEach(([p, a]) => {
189
- console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${a}`));
216
+ // Spread line
217
+ const spreadStr = spreadAbs > 0
218
+ ? `$${formatUsd(spreadAbs)} (${spreadPct.toFixed(3)}%)`
219
+ : "─";
220
+ console.log(chalk.gray(` ${"─".repeat(16)} spread: ${spreadStr} ${"─".repeat(10)}`));
221
+ bids.forEach(([p, a], i) => {
222
+ const cumUsd = bidCums[i] * (Number(p));
223
+ console.log(chalk.green(` ${formatUsd(p).padStart(12)} ${String(a).padEnd(12)} $${formatUsd(cumUsd).padEnd(14)}`) +
224
+ chalk.green.dim(bar(bidCums[i])));
190
225
  });
191
226
  console.log();
192
227
  });
@@ -69,8 +69,8 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
69
69
  reduceOnly?: boolean;
70
70
  tif?: string;
71
71
  }): Promise<unknown>;
72
- cancelOrder(symbol: string, orderId: string): Promise<import("hyperliquid").CancelOrderResponse>;
73
- cancelAllOrders(symbol?: string): Promise<import("hyperliquid").CancelOrderResponse[]>;
72
+ cancelOrder(symbol: string, orderId: string): Promise<unknown>;
73
+ cancelAllOrders(symbol?: string): Promise<unknown[]>;
74
74
  editOrder(symbol: string, orderId: string, price: string, size: string): Promise<unknown>;
75
75
  setLeverage(symbol: string, leverage: number, marginMode?: "cross" | "isolated"): Promise<any>;
76
76
  stopOrder(symbol: string, side: "buy" | "sell", size: string, triggerPrice: string, opts?: {
@@ -72,17 +72,38 @@ export class HyperliquidAdapter {
72
72
  async _loadAssetMap() {
73
73
  try {
74
74
  if (this._dex) {
75
- // HIP-3 deployed dex: use raw info POST with dex param
76
- const meta = await this._infoPost({ type: "meta", dex: this._dex });
75
+ // HIP-3 deployed dex: asset indices use global offset scheme
76
+ // offset = 110000 + dexListIndex * 10000 (per Python SDK convention)
77
+ const [meta, dexList] = await Promise.all([
78
+ this._infoPost({ type: "meta", dex: this._dex }),
79
+ this._infoPost({ type: "perpDexs" }),
80
+ ]);
81
+ // Find this dex's position in the list (index 0 is native/null)
82
+ let dexOffset = 0;
83
+ for (let i = 1; i < dexList.length; i++) {
84
+ if (dexList[i]?.name === this._dex) {
85
+ dexOffset = 110000 + (i - 1) * 10000;
86
+ break;
87
+ }
88
+ }
89
+ if (dexOffset === 0) {
90
+ console.error(`[hyperliquid] Dex "${this._dex}" not found in perpDexs list`);
91
+ }
77
92
  if (meta?.universe) {
78
93
  meta.universe.forEach((asset, idx) => {
94
+ const globalIdx = dexOffset + idx;
79
95
  // Store both with and without dex prefix for flexible lookup
80
96
  // API returns "km:GOOGL" but callers may use "GOOGL" or "KM:GOOGL"
81
97
  const fullName = asset.name; // e.g., "km:GOOGL"
82
98
  const baseName = fullName.includes(":") ? fullName.split(":").pop() : fullName;
83
- this._assetMap.set(fullName, idx);
84
- this._assetMap.set(baseName, idx);
85
- this._assetMapReverse.set(idx, fullName);
99
+ this._assetMap.set(fullName, globalIdx);
100
+ this._assetMap.set(baseName, globalIdx);
101
+ this._assetMapReverse.set(globalIdx, fullName);
102
+ // Cache szDecimals for dex assets (critical for order sizing)
103
+ if (asset.szDecimals !== undefined) {
104
+ this._szDecimalsMap.set(fullName, asset.szDecimals);
105
+ this._szDecimalsMap.set(baseName, asset.szDecimals);
106
+ }
86
107
  });
87
108
  }
88
109
  }
@@ -168,8 +189,8 @@ export class HyperliquidAdapter {
168
189
  ctxs = meta[1] ?? [];
169
190
  mids = allMids;
170
191
  }
171
- // Rebuild asset map if empty
172
- if (this._assetMap.size === 0) {
192
+ // Rebuild asset map if empty (fallback — normally populated by init/_loadAssetMap)
193
+ if (this._assetMap.size === 0 && !this._dex) {
173
194
  universe.forEach((asset, i) => {
174
195
  const sym = String(asset.name);
175
196
  this._assetMap.set(sym, i);
@@ -191,8 +212,24 @@ export class HyperliquidAdapter {
191
212
  });
192
213
  }
193
214
  async getOrderbook(symbol) {
194
- const book = await this.sdk.info.getL2Book(symbol.toUpperCase());
195
- const levels = book?.levels ?? [[], []];
215
+ let levels;
216
+ if (this._dex) {
217
+ // HIP-3 dex: SDK's getL2Book() doesn't support dex parameter
218
+ // Resolve symbol to full dex-prefixed name (e.g. "US500" → "km:US500")
219
+ const resolved = this.resolveSymbol(symbol.toUpperCase());
220
+ // Ensure dex prefix is present (API requires it)
221
+ const coin = resolved.includes(":") ? resolved : `${this._dex}:${resolved}`;
222
+ const book = await this._infoPost({
223
+ type: "l2Book",
224
+ coin,
225
+ dex: this._dex,
226
+ });
227
+ levels = book?.levels ?? [[], []];
228
+ }
229
+ else {
230
+ const book = await this.sdk.info.getL2Book(symbol.toUpperCase());
231
+ levels = book?.levels ?? [[], []];
232
+ }
196
233
  return {
197
234
  bids: (levels[0] ?? []).map((l) => [
198
235
  String(l.px ?? "0"),
@@ -291,6 +328,22 @@ export class HyperliquidAdapter {
291
328
  }
292
329
  async getOpenOrders() {
293
330
  this.ensureAddress();
331
+ if (this._dex) {
332
+ // HIP-3 dex: use frontendOpenOrders with dex parameter
333
+ const orders = await this._infoPost({
334
+ type: "frontendOpenOrders", user: this._address, dex: this._dex,
335
+ });
336
+ return (orders ?? []).map((o) => ({
337
+ orderId: String(o.oid ?? ""),
338
+ symbol: String(o.coin ?? ""),
339
+ side: String(o.side) === "B" ? "buy" : String(o.side) === "A" ? "sell" : String(o.side).toLowerCase(),
340
+ price: String(o.limitPx ?? "0"),
341
+ size: String(o.sz ?? "0"),
342
+ filled: "0",
343
+ status: "open",
344
+ type: "limit",
345
+ }));
346
+ }
294
347
  const orders = await this.sdk.info.getUserOpenOrders(this._address);
295
348
  return (orders ?? []).map((o) => ({
296
349
  orderId: String(o.oid ?? ""),
@@ -337,16 +390,20 @@ export class HyperliquidAdapter {
337
390
  */
338
391
  async _dexMarketOrder(symbol, side, size) {
339
392
  const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
340
- // Get current mark price + szDecimals from dex meta
393
+ const szDec = this.getSzDecimals(symbol);
394
+ // Get current mark price from dex meta (lookup by coin name, not array index)
395
+ const resolved = this.resolveSymbol(symbol.toUpperCase());
396
+ const coin = resolved.includes(":") ? resolved : `${this._dex}:${resolved}`;
341
397
  const meta = await this._infoPost({
342
398
  type: "metaAndAssetCtxs",
343
399
  dex: this._dex,
344
400
  });
345
- const ctx = (meta[1] ?? [])[assetIndex];
401
+ // Find local index by matching coin name in universe
402
+ const localIdx = meta[0]?.universe?.findIndex(a => a.name === coin) ?? -1;
403
+ const ctx = localIdx >= 0 ? (meta[1] ?? [])[localIdx] : undefined;
346
404
  const midPrice = Number(ctx?.markPx ?? 0);
347
405
  if (midPrice <= 0)
348
406
  throw new Error(`Cannot get price for ${symbol} on dex ${this._dex}`);
349
- const szDec = meta[0]?.universe?.[assetIndex]?.szDecimals ?? 2;
350
407
  // HL official rounding: 5 significant figures, max 6 decimals for perps
351
408
  const MAX_DECIMALS_PERP = 6;
352
409
  const slippage = 0.05;
@@ -419,7 +476,14 @@ export class HyperliquidAdapter {
419
476
  headers: { "Content-Type": "application/json" },
420
477
  body: JSON.stringify(payload),
421
478
  });
422
- const result = await res.json();
479
+ const text = await res.text();
480
+ let result;
481
+ try {
482
+ result = JSON.parse(text);
483
+ }
484
+ catch {
485
+ throw new Error(`HL exchange API error (${res.status}): ${text.slice(0, 200)}`);
486
+ }
423
487
  if (result?.status === "err") {
424
488
  throw new Error(result.response ?? JSON.stringify(result));
425
489
  }
@@ -450,10 +514,7 @@ export class HyperliquidAdapter {
450
514
  orders: [orderWire],
451
515
  grouping: "na",
452
516
  };
453
- // Add dex field for HIP-3 deployed perps
454
- if (this._dex) {
455
- action.dex = this._dex;
456
- }
517
+ // No "dex" field needed — the global asset index (110000+) encodes the dex
457
518
  return this._signAndSendAction(action);
458
519
  }
459
520
  async limitOrder(symbol, side, price, size, opts) {
@@ -480,6 +541,16 @@ export class HyperliquidAdapter {
480
541
  }
481
542
  async cancelOrder(symbol, orderId) {
482
543
  this.ensureSigner();
544
+ if (this._dex) {
545
+ // HIP-3 dex: use raw cancel action with global asset index
546
+ const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
547
+ const result = await this._signAndSendAction({
548
+ type: "cancel",
549
+ cancels: [{ a: assetIndex, o: parseInt(orderId) }],
550
+ });
551
+ await this._invalidateAccountCache();
552
+ return result;
553
+ }
483
554
  // HL SDK expects coin in "ETH-PERP" format (internal symbol convention)
484
555
  const resolved = this.resolveSymbol(symbol);
485
556
  const result = await this.sdk.exchange.cancelOrder({
@@ -494,16 +565,25 @@ export class HyperliquidAdapter {
494
565
  const orders = await this.getOpenOrders();
495
566
  const toCancel = symbol
496
567
  ? orders.filter((o) => {
497
- const norm = (s) => s.toUpperCase().replace(/-PERP$/, "");
568
+ const norm = (s) => s.toUpperCase().replace(/-PERP$/, "").replace(/^[^:]+:/, "");
498
569
  return norm(o.symbol) === norm(symbol);
499
570
  })
500
571
  : orders;
501
572
  const results = [];
502
573
  for (const o of toCancel) {
503
- results.push(await this.sdk.exchange.cancelOrder({
504
- coin: o.symbol,
505
- o: parseInt(o.orderId),
506
- }));
574
+ if (this._dex) {
575
+ const assetIndex = await this.getAssetIndex(o.symbol);
576
+ results.push(await this._signAndSendAction({
577
+ type: "cancel",
578
+ cancels: [{ a: assetIndex, o: parseInt(o.orderId) }],
579
+ }));
580
+ }
581
+ else {
582
+ results.push(await this.sdk.exchange.cancelOrder({
583
+ coin: o.symbol,
584
+ o: parseInt(o.orderId),
585
+ }));
586
+ }
507
587
  }
508
588
  return results;
509
589
  }
@@ -608,22 +688,37 @@ export class HyperliquidAdapter {
608
688
  */
609
689
  async triggerOrder(symbol, side, size, triggerPrice, tpsl, opts) {
610
690
  this.ensureSigner();
611
- const orderParams = {
612
- coin: symbol.toUpperCase(),
613
- is_buy: side === "buy",
614
- sz: parseFloat(size),
615
- limit_px: parseFloat(triggerPrice),
616
- order_type: {
691
+ const assetIndex = await this.getAssetIndex(symbol.toUpperCase());
692
+ const szDec = this.getSzDecimals(symbol);
693
+ const roundedSize = Number(size).toFixed(szDec);
694
+ const roundedTrigger = this._hlRoundPrice(Number(triggerPrice), szDec);
695
+ // Remove trailing zeros (HL API requirement)
696
+ const trimZeros = (s) => {
697
+ if (!s.includes("."))
698
+ return s;
699
+ const n = s.replace(/\.?0+$/, "");
700
+ return n === "-0" ? "0" : n;
701
+ };
702
+ const orderWire = {
703
+ a: assetIndex,
704
+ b: side === "buy",
705
+ p: trimZeros(roundedTrigger),
706
+ s: trimZeros(roundedSize),
707
+ r: opts?.reduceOnly ?? true,
708
+ t: {
617
709
  trigger: {
618
- triggerPx: triggerPrice,
710
+ triggerPx: trimZeros(roundedTrigger),
619
711
  isMarket: opts?.isMarket ?? true,
620
712
  tpsl,
621
713
  },
622
714
  },
623
- reduce_only: opts?.reduceOnly ?? true,
715
+ };
716
+ const action = {
717
+ type: "order",
718
+ orders: [orderWire],
624
719
  grouping: opts?.grouping ?? "positionTpsl",
625
720
  };
626
- return this.sdk.exchange.placeOrder(orderParams);
721
+ return this._signAndSendAction(action);
627
722
  }
628
723
  /**
629
724
  * Place a TWAP order.
@@ -140,30 +140,15 @@ export class LighterSpotAdapter {
140
140
  ?? res.accounts?.[0];
141
141
  if (!acct)
142
142
  return [];
143
- // Perp USDC balance (collateral / available for perp trading)
144
- balances.push({
145
- token: "USDC",
146
- total: String(acct.collateral ?? 0),
147
- available: String(acct.available_balance ?? 0),
148
- held: String(Math.max(0, Number(acct.collateral ?? 0) - Number(acct.available_balance ?? 0))),
149
- });
150
- // Spot token balances from the assets array
143
+ // Spot token balances from the assets array only.
144
+ // Perp USDC (collateral) is already reported by getBalance() — don't duplicate here.
151
145
  if (acct.assets && acct.assets.length > 0) {
152
146
  for (const asset of acct.assets) {
153
147
  const bal = parseFloat(asset.balance || "0");
154
148
  const locked = parseFloat(asset.locked_balance || "0");
155
- if (asset.symbol === "USDC") {
156
- // Add spot USDC as separate entry so caller can distinguish
157
- balances.push({
158
- token: "USDC_SPOT",
159
- total: asset.balance,
160
- available: String(bal - locked),
161
- held: asset.locked_balance,
162
- });
163
- }
164
- else if (bal > 0) {
149
+ if (bal > 0) {
165
150
  balances.push({
166
- token: asset.symbol,
151
+ token: asset.symbol === "USDC" ? "USDC_SPOT" : asset.symbol,
167
152
  total: asset.balance,
168
153
  available: String(bal - locked),
169
154
  held: asset.locked_balance,
@@ -240,10 +240,19 @@ export class LighterAdapter {
240
240
  if (!dec) {
241
241
  throw new Error(`No market decimals loaded for ${symbol}. Market data may not be initialized.`);
242
242
  }
243
- return {
244
- baseAmount: Math.round(size * Math.pow(10, dec.size)),
245
- priceTicks: Math.round(price * Math.pow(10, dec.price)),
246
- };
243
+ const baseAmount = Math.round(size * Math.pow(10, dec.size));
244
+ const priceTicks = Math.round(price * Math.pow(10, dec.price));
245
+ // Validate: size too small for this market's precision → baseAmount rounds to 0
246
+ if (size > 0 && baseAmount <= 0) {
247
+ const minSize = 1 / Math.pow(10, dec.size);
248
+ throw new Error(`Order size ${size} too small for ${symbol} (sizeDecimals=${dec.size}, min=${minSize}). ` +
249
+ `Increase size or use a different market.`);
250
+ }
251
+ if (price > 0 && priceTicks <= 0) {
252
+ const minPrice = 1 / Math.pow(10, dec.price);
253
+ throw new Error(`Order price ${price} too small for ${symbol} (priceDecimals=${dec.price}, min=${minPrice}).`);
254
+ }
255
+ return { baseAmount, priceTicks };
247
256
  }
248
257
  async getMarkets() {
249
258
  const markets = [];
@@ -338,9 +347,14 @@ export class LighterAdapter {
338
347
  : "0"),
339
348
  liquidationPrice: String(p.liquidation_price || "N/A"),
340
349
  unrealizedPnl: String(p.unrealized_pnl || "0"),
341
- leverage: Number(p.initial_margin_fraction || 0) > 0
342
- ? Math.round(10000 / Number(p.initial_margin_fraction))
343
- : 1,
350
+ // Compute actual leverage = notional / account equity (not max leverage from IMF)
351
+ leverage: (() => {
352
+ const notional = Math.abs(Number(p.position_value || 0));
353
+ const equity = Number(acct.total_asset_value || 0);
354
+ if (equity > 0 && notional > 0)
355
+ return Math.round(notional / equity * 10) / 10;
356
+ return 1;
357
+ })(),
344
358
  };
345
359
  });
346
360
  }
package/dist/liquidity.js CHANGED
@@ -41,13 +41,15 @@ export function computeExecutableSize(levels, requestedSizeUsd, maxSlippagePct =
41
41
  const avgFillPrice = filledSize > 0 ? filledNotional / filledSize : bestPrice;
42
42
  const slippagePct = bestPrice > 0 ? ((avgFillPrice - bestPrice) / bestPrice) * 100 : 0;
43
43
  const canFillFull = filledNotional >= requestedSizeUsd * 0.95; // 95% fill = close enough
44
+ // 10% safety buffer — avoid filling at the very edge of liquidity
45
+ const bufferedSize = filledSize * 0.9;
44
46
  return {
45
47
  maxSize: filledSize,
46
48
  avgFillPrice,
47
49
  slippagePct: Math.abs(slippagePct),
48
50
  depthUsd: totalDepthUsd,
49
51
  canFillFull,
50
- recommendedSize: filledSize,
52
+ recommendedSize: bufferedSize,
51
53
  };
52
54
  }
53
55
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.4.11",
3
+ "version": "0.5.0",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",