perp-cli 0.4.9 → 0.4.11
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.
- package/dist/commands/account.js +254 -47
- package/dist/commands/agent.js +4 -4
- package/dist/commands/market.d.ts +1 -1
- package/dist/commands/market.js +87 -1
- package/dist/index.js +1 -1
- package/dist/mcp-server.d.ts +1 -1
- package/dist/mcp-server.js +15 -15
- package/package.json +1 -1
- package/skills/perp-cli/references/agent-operations.md +7 -7
- package/skills/perp-cli/references/commands.md +1 -1
- package/skills/perp-cli/references/strategies.md +2 -2
- package/skills/perp-cli/scripts/validate-arb.sh +2 -2
package/dist/commands/account.js
CHANGED
|
@@ -101,30 +101,21 @@ function pac(adapter) {
|
|
|
101
101
|
}
|
|
102
102
|
export function registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange) {
|
|
103
103
|
const account = program.command("account").description("Account commands");
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
.alias("info")
|
|
107
|
-
.description("Account overview: perp balance, spot holdings, and 24h funding")
|
|
108
|
-
.action(async () => {
|
|
109
|
-
const adapter = await getAdapter();
|
|
110
|
-
const exName = adapter.name.toLowerCase();
|
|
111
|
-
// Fetch perp balance + funding in parallel
|
|
104
|
+
async function fetchExchangeBalance(exName, adapter) {
|
|
105
|
+
const name = exName.toLowerCase();
|
|
112
106
|
const [bal, fundingPayments] = await Promise.all([
|
|
113
107
|
adapter.getBalance(),
|
|
114
108
|
adapter.getFundingPayments(200).catch(() => []),
|
|
115
109
|
]);
|
|
116
|
-
// Spot balances (HL and LT only)
|
|
117
110
|
let spotBalances = [];
|
|
118
111
|
try {
|
|
119
|
-
if (
|
|
112
|
+
if (name === "hyperliquid") {
|
|
120
113
|
const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
|
|
121
114
|
const hlSpot = new HyperliquidSpotAdapter(adapter);
|
|
122
115
|
await hlSpot.init();
|
|
123
116
|
const raw = await hlSpot.getSpotBalances();
|
|
124
|
-
// Get spot prices for USD valuation
|
|
125
117
|
const markets = await hlSpot.getSpotMarkets();
|
|
126
118
|
const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
|
|
127
|
-
// HL spot API returns token names with -SPOT suffix (e.g., "HYPE-SPOT", "USDC-SPOT")
|
|
128
119
|
const stripSuffix = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
|
|
129
120
|
spotBalances = raw
|
|
130
121
|
.filter(b => Number(b.total) > 0)
|
|
@@ -136,7 +127,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
136
127
|
};
|
|
137
128
|
});
|
|
138
129
|
}
|
|
139
|
-
else if (
|
|
130
|
+
else if (name === "lighter") {
|
|
140
131
|
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
|
141
132
|
const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
|
|
142
133
|
const ltSpot = new LighterSpotAdapter(adapter);
|
|
@@ -155,33 +146,52 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
155
146
|
}
|
|
156
147
|
}
|
|
157
148
|
catch { /* spot not available */ }
|
|
158
|
-
//
|
|
149
|
+
// HL unified account: perp equity already includes spot USDC balance.
|
|
150
|
+
// Detect by checking if adapter is HL without a dex override.
|
|
151
|
+
const isUnified = name === "hyperliquid" && !adapter.dex;
|
|
159
152
|
const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
|
|
160
153
|
const recent = fundingPayments.filter(f => f.time >= cutoff24h);
|
|
161
154
|
const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
|
|
162
|
-
// Total spot value
|
|
163
155
|
const totalSpotUsd = spotBalances.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
// For unified accounts, equity already contains USDC spot value.
|
|
157
|
+
// Only add non-USDC spot token values to avoid double-counting.
|
|
158
|
+
const nonUsdcSpotUsd = isUnified
|
|
159
|
+
? spotBalances
|
|
160
|
+
.filter(b => {
|
|
161
|
+
const tk = b.token.replace(/-SPOT$/i, "").toUpperCase();
|
|
162
|
+
return tk !== "USDC";
|
|
163
|
+
})
|
|
164
|
+
.reduce((s, b) => s + (b.valueUsd ?? 0), 0)
|
|
165
|
+
: totalSpotUsd;
|
|
166
|
+
return {
|
|
167
|
+
exchange: name,
|
|
168
|
+
perp: bal,
|
|
169
|
+
spot: spotBalances,
|
|
170
|
+
funding24h,
|
|
171
|
+
fundingPayments24h: recent.length,
|
|
172
|
+
totalSpotValueUsd: totalSpotUsd,
|
|
173
|
+
totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotUsd,
|
|
174
|
+
unifiedAccount: isUnified,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function printSingleBalance(exName, r) {
|
|
178
|
+
console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance`));
|
|
179
|
+
if (r.unifiedAccount)
|
|
180
|
+
console.log(chalk.gray(" (Unified account — perp & spot share single balance)\n"));
|
|
181
|
+
else
|
|
182
|
+
console.log();
|
|
183
|
+
console.log(chalk.white.bold(r.unifiedAccount ? " Account" : " Perp Account"));
|
|
184
|
+
console.log(` Equity: $${formatUsd(r.perp.equity)}`);
|
|
185
|
+
console.log(` Available: $${formatUsd(r.perp.available)}`);
|
|
186
|
+
console.log(` Margin Used: $${formatUsd(r.perp.marginUsed)}`);
|
|
187
|
+
console.log(` Unreal. PnL: ${formatPnl(r.perp.unrealizedPnl)}`);
|
|
188
|
+
// For unified, show non-USDC spot holdings (USDC is already in equity)
|
|
189
|
+
const displaySpot = r.unifiedAccount
|
|
190
|
+
? r.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
191
|
+
: r.spot;
|
|
192
|
+
if (displaySpot.length > 0) {
|
|
183
193
|
console.log(chalk.white.bold("\n Spot Holdings"));
|
|
184
|
-
const rows =
|
|
194
|
+
const rows = displaySpot.map(b => [
|
|
185
195
|
chalk.white.bold(b.token),
|
|
186
196
|
b.total,
|
|
187
197
|
b.available,
|
|
@@ -190,27 +200,176 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
190
200
|
]);
|
|
191
201
|
console.log(makeTable(["Token", "Total", "Available", "Held", "Value"], rows));
|
|
192
202
|
}
|
|
193
|
-
else if (exName === "hyperliquid" || exName === "lighter") {
|
|
194
|
-
console.log(chalk.gray("\n No spot holdings.\n"));
|
|
195
|
-
}
|
|
196
|
-
// ── Funding 24h ──
|
|
197
203
|
console.log(chalk.white.bold(" Funding (24h)"));
|
|
198
|
-
console.log(` Payments: ${
|
|
199
|
-
console.log(` Total: ${formatPnl(funding24h)}`);
|
|
200
|
-
//
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
204
|
+
console.log(` Payments: ${r.fundingPayments24h}`);
|
|
205
|
+
console.log(` Total: ${formatPnl(r.funding24h)}`);
|
|
206
|
+
// Only show Total breakdown when there are non-USDC spot tokens adding value
|
|
207
|
+
const nonUsdcSpotValue = displaySpot.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
|
|
208
|
+
if (nonUsdcSpotValue > 0) {
|
|
203
209
|
console.log(chalk.white.bold("\n Total"));
|
|
204
|
-
console.log(`
|
|
205
|
-
console.log(` Spot
|
|
206
|
-
console.log(` Total Value: $${formatUsd(
|
|
210
|
+
console.log(` Equity: $${formatUsd(r.perp.equity)}`);
|
|
211
|
+
console.log(` Spot (other): $${formatUsd(nonUsdcSpotValue)}`);
|
|
212
|
+
console.log(` Total Value: $${formatUsd(r.totalAccountValueUsd)}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
account
|
|
216
|
+
.command("balance")
|
|
217
|
+
.description("Account overview: perp balance, spot holdings, and 24h funding")
|
|
218
|
+
.action(async () => {
|
|
219
|
+
// Detect if user explicitly set -e (single exchange mode)
|
|
220
|
+
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
221
|
+
// Multi-exchange mode: show all exchanges
|
|
222
|
+
if (!explicitExchange && getAdapterForExchange) {
|
|
223
|
+
const results = [];
|
|
224
|
+
const errors = {};
|
|
225
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
226
|
+
try {
|
|
227
|
+
const adapter = await getAdapterForExchange(ex);
|
|
228
|
+
results.push(await fetchExchangeBalance(ex, adapter));
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
232
|
+
}
|
|
233
|
+
}));
|
|
234
|
+
// Sort by equity desc
|
|
235
|
+
results.sort((a, b) => b.totalAccountValueUsd - a.totalAccountValueUsd);
|
|
236
|
+
if (isJson()) {
|
|
237
|
+
const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
|
|
238
|
+
return printJson(jsonOk({
|
|
239
|
+
exchanges: results,
|
|
240
|
+
errors: Object.keys(errors).length > 0 ? errors : undefined,
|
|
241
|
+
totalEquity: results.reduce((s, r) => s + Number(r.perp.equity), 0),
|
|
242
|
+
totalSpotValueUsd: results.reduce((s, r) => s + r.totalSpotValueUsd, 0),
|
|
243
|
+
totalAccountValueUsd: grandTotal,
|
|
244
|
+
totalFunding24h: results.reduce((s, r) => s + r.funding24h, 0),
|
|
245
|
+
}));
|
|
246
|
+
}
|
|
247
|
+
console.log(chalk.cyan.bold("\n All Exchanges — Account Balance\n"));
|
|
248
|
+
// Summary table
|
|
249
|
+
// For unified accounts, Spot column shows non-USDC only (USDC already in Equity)
|
|
250
|
+
const balRows = results.map(r => {
|
|
251
|
+
const nonUsdcSpot = r.unifiedAccount
|
|
252
|
+
? r.spot
|
|
253
|
+
.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
254
|
+
.reduce((s, b) => s + (b.valueUsd ?? 0), 0)
|
|
255
|
+
: r.totalSpotValueUsd;
|
|
256
|
+
return [
|
|
257
|
+
chalk.white.bold(r.exchange) + (r.unifiedAccount ? chalk.gray(" ★") : ""),
|
|
258
|
+
`$${formatUsd(r.perp.equity)}`,
|
|
259
|
+
`$${formatUsd(r.perp.available)}`,
|
|
260
|
+
`$${formatUsd(r.perp.marginUsed)}`,
|
|
261
|
+
nonUsdcSpot > 0 ? `$${formatUsd(nonUsdcSpot)}` : chalk.gray("-"),
|
|
262
|
+
formatPnl(r.funding24h),
|
|
263
|
+
`$${formatUsd(r.totalAccountValueUsd)}`,
|
|
264
|
+
];
|
|
265
|
+
});
|
|
266
|
+
const grandEquity = results.reduce((s, r) => s + Number(r.perp.equity), 0);
|
|
267
|
+
const grandSpot = results.reduce((s, r) => {
|
|
268
|
+
if (r.unifiedAccount) {
|
|
269
|
+
return s + r.spot
|
|
270
|
+
.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
271
|
+
.reduce((ss, b) => ss + (b.valueUsd ?? 0), 0);
|
|
272
|
+
}
|
|
273
|
+
return s + r.totalSpotValueUsd;
|
|
274
|
+
}, 0);
|
|
275
|
+
const grandFunding = results.reduce((s, r) => s + r.funding24h, 0);
|
|
276
|
+
const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
|
|
277
|
+
balRows.push([
|
|
278
|
+
chalk.cyan.bold("TOTAL"),
|
|
279
|
+
chalk.cyan.bold(`$${formatUsd(grandEquity)}`),
|
|
280
|
+
"",
|
|
281
|
+
"",
|
|
282
|
+
grandSpot > 0 ? chalk.cyan.bold(`$${formatUsd(grandSpot)}`) : "",
|
|
283
|
+
formatPnl(grandFunding),
|
|
284
|
+
chalk.cyan.bold(`$${formatUsd(grandTotal)}`),
|
|
285
|
+
]);
|
|
286
|
+
console.log(makeTable(["Exchange", "Equity", "Available", "Margin", "Spot", "Funding 24h", "Total"], balRows));
|
|
287
|
+
if (results.some(r => r.unifiedAccount)) {
|
|
288
|
+
console.log(chalk.gray(" ★ Unified account — Equity includes spot USDC, Spot shows non-USDC tokens only\n"));
|
|
289
|
+
}
|
|
290
|
+
// Spot details per exchange (for unified, skip USDC as it's in equity)
|
|
291
|
+
for (const r of results) {
|
|
292
|
+
const displaySpot = r.unifiedAccount
|
|
293
|
+
? r.spot.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
294
|
+
: r.spot;
|
|
295
|
+
if (displaySpot.length > 0) {
|
|
296
|
+
console.log(chalk.white.bold(` ${r.exchange.toUpperCase()} Spot Holdings`));
|
|
297
|
+
const rows = displaySpot.map(b => [
|
|
298
|
+
chalk.white.bold(b.token),
|
|
299
|
+
b.total,
|
|
300
|
+
b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
|
|
301
|
+
]);
|
|
302
|
+
console.log(makeTable(["Token", "Total", "Value"], rows));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Errors
|
|
306
|
+
for (const [ex, msg] of Object.entries(errors)) {
|
|
307
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
308
|
+
}
|
|
309
|
+
console.log();
|
|
310
|
+
return;
|
|
207
311
|
}
|
|
312
|
+
// Single exchange mode (explicit -e or no getAdapterForExchange)
|
|
313
|
+
const adapter = await getAdapter();
|
|
314
|
+
const exName = adapter.name.toLowerCase();
|
|
315
|
+
const result = await fetchExchangeBalance(exName, adapter);
|
|
316
|
+
if (isJson())
|
|
317
|
+
return printJson(jsonOk(result));
|
|
318
|
+
printSingleBalance(exName, result);
|
|
208
319
|
console.log();
|
|
209
320
|
});
|
|
210
321
|
account
|
|
211
322
|
.command("positions")
|
|
212
323
|
.description("Show open positions")
|
|
213
324
|
.action(async () => {
|
|
325
|
+
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
326
|
+
if (!explicitExchange && getAdapterForExchange) {
|
|
327
|
+
const allRows = [];
|
|
328
|
+
const errors = {};
|
|
329
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
330
|
+
try {
|
|
331
|
+
const adapter = await getAdapterForExchange(ex);
|
|
332
|
+
const positions = await adapter.getPositions();
|
|
333
|
+
for (const p of positions) {
|
|
334
|
+
allRows.push([
|
|
335
|
+
chalk.white.bold(p.symbol),
|
|
336
|
+
chalk.gray(ex.slice(0, 3).toUpperCase()),
|
|
337
|
+
p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
|
|
338
|
+
p.size,
|
|
339
|
+
`$${formatUsd(p.entryPrice)}`,
|
|
340
|
+
`$${formatUsd(p.markPrice)}`,
|
|
341
|
+
p.liquidationPrice === "N/A" ? chalk.gray("N/A") : `$${formatUsd(p.liquidationPrice)}`,
|
|
342
|
+
formatPnl(p.unrealizedPnl),
|
|
343
|
+
`${p.leverage}x`,
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch (err) {
|
|
348
|
+
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
349
|
+
}
|
|
350
|
+
}));
|
|
351
|
+
if (isJson()) {
|
|
352
|
+
const grouped = {};
|
|
353
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
354
|
+
try {
|
|
355
|
+
const adapter = await getAdapterForExchange(ex);
|
|
356
|
+
grouped[ex] = await adapter.getPositions();
|
|
357
|
+
}
|
|
358
|
+
catch { /* skip */ }
|
|
359
|
+
}));
|
|
360
|
+
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
361
|
+
}
|
|
362
|
+
if (allRows.length === 0) {
|
|
363
|
+
console.log(chalk.gray("\n No open positions across all exchanges.\n"));
|
|
364
|
+
for (const [ex, msg] of Object.entries(errors))
|
|
365
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
console.log(makeTable(["Symbol", "Exch", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], allRows));
|
|
369
|
+
for (const [ex, msg] of Object.entries(errors))
|
|
370
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
214
373
|
const adapter = await getAdapter();
|
|
215
374
|
const positions = await adapter.getPositions();
|
|
216
375
|
if (isJson())
|
|
@@ -235,6 +394,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
235
394
|
.command("orders")
|
|
236
395
|
.description("Show open orders")
|
|
237
396
|
.action(async () => {
|
|
397
|
+
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
398
|
+
if (!explicitExchange && getAdapterForExchange) {
|
|
399
|
+
const allRows = [];
|
|
400
|
+
const errors = {};
|
|
401
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
402
|
+
try {
|
|
403
|
+
const adapter = await getAdapterForExchange(ex);
|
|
404
|
+
const orders = await adapter.getOpenOrders();
|
|
405
|
+
for (const o of orders) {
|
|
406
|
+
allRows.push([
|
|
407
|
+
o.orderId,
|
|
408
|
+
chalk.white.bold(o.symbol),
|
|
409
|
+
chalk.gray(ex.slice(0, 3).toUpperCase()),
|
|
410
|
+
o.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
|
|
411
|
+
o.type,
|
|
412
|
+
`$${formatUsd(o.price)}`,
|
|
413
|
+
o.size,
|
|
414
|
+
o.filled,
|
|
415
|
+
o.status,
|
|
416
|
+
]);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
421
|
+
}
|
|
422
|
+
}));
|
|
423
|
+
if (isJson()) {
|
|
424
|
+
const grouped = {};
|
|
425
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
426
|
+
try {
|
|
427
|
+
const adapter = await getAdapterForExchange(ex);
|
|
428
|
+
grouped[ex] = await adapter.getOpenOrders();
|
|
429
|
+
}
|
|
430
|
+
catch { /* skip */ }
|
|
431
|
+
}));
|
|
432
|
+
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
433
|
+
}
|
|
434
|
+
if (allRows.length === 0) {
|
|
435
|
+
console.log(chalk.gray("\n No open orders across all exchanges.\n"));
|
|
436
|
+
for (const [ex, msg] of Object.entries(errors))
|
|
437
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
console.log(makeTable(["ID", "Symbol", "Exch", "Side", "Type", "Price", "Size", "Filled", "Status"], allRows));
|
|
441
|
+
for (const [ex, msg] of Object.entries(errors))
|
|
442
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
238
445
|
const adapter = await getAdapter();
|
|
239
446
|
const orders = await adapter.getOpenOrders();
|
|
240
447
|
if (isJson())
|
package/dist/commands/agent.js
CHANGED
|
@@ -136,7 +136,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
|
|
|
136
136
|
{
|
|
137
137
|
category: "account",
|
|
138
138
|
commands: [
|
|
139
|
-
"perp account
|
|
139
|
+
"perp account balance --json",
|
|
140
140
|
"perp account positions --json",
|
|
141
141
|
"perp account orders --json",
|
|
142
142
|
"perp account history --json",
|
|
@@ -338,12 +338,12 @@ export function registerAgentCommands(program, getAdapter, isJson) {
|
|
|
338
338
|
if (g.includes("buy") || g.includes("long")) {
|
|
339
339
|
const symbol = extractSymbol(g) || "BTC";
|
|
340
340
|
const size = extractNumber(g) || "0.01";
|
|
341
|
-
steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account
|
|
341
|
+
steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account balance --json`, description: "Check available balance" }, { step: 3, command: `perp trade market ${symbol} buy ${size} --json`, description: `Buy ${size} ${symbol}` }, { step: 4, command: `perp account positions --json`, description: "Verify position opened" });
|
|
342
342
|
}
|
|
343
343
|
else if (g.includes("sell") || g.includes("short")) {
|
|
344
344
|
const symbol = extractSymbol(g) || "BTC";
|
|
345
345
|
const size = extractNumber(g) || "0.01";
|
|
346
|
-
steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account
|
|
346
|
+
steps.push({ step: 1, command: `perp market book ${symbol} --json`, description: `Check ${symbol} orderbook` }, { step: 2, command: `perp account balance --json`, description: "Check available balance" }, { step: 3, command: `perp trade market ${symbol} sell ${size} --json`, description: `Sell ${size} ${symbol}` }, { step: 4, command: `perp account positions --json`, description: "Verify position opened" });
|
|
347
347
|
}
|
|
348
348
|
else if (g.includes("close") || g.includes("exit")) {
|
|
349
349
|
const symbol = extractSymbol(g) || "ALL";
|
|
@@ -360,7 +360,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
|
|
|
360
360
|
}
|
|
361
361
|
else if (g.includes("deposit")) {
|
|
362
362
|
const amount = extractNumber(g) || "100";
|
|
363
|
-
steps.push({ step: 1, command: "perp wallet balance --json", description: "Check wallet balance" }, { step: 2, command: `perp deposit pacifica ${amount} --json`, description: `Deposit $${amount} to Pacifica` }, { step: 3, command: "perp account
|
|
363
|
+
steps.push({ step: 1, command: "perp wallet balance --json", description: "Check wallet balance" }, { step: 2, command: `perp deposit pacifica ${amount} --json`, description: `Deposit $${amount} to Pacifica` }, { step: 3, command: "perp account balance --json", description: "Verify deposit arrived" });
|
|
364
364
|
}
|
|
365
365
|
else if (g.includes("price") || g.includes("market")) {
|
|
366
366
|
const symbol = extractSymbol(g);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
3
|
-
export declare function registerMarketCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean): void;
|
|
3
|
+
export declare function registerMarketCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
|
package/dist/commands/market.js
CHANGED
|
@@ -2,12 +2,59 @@ import { PacificaAdapter } from "../exchanges/pacifica.js";
|
|
|
2
2
|
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
3
3
|
import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
|
-
|
|
5
|
+
const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
|
|
6
|
+
export function registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange) {
|
|
6
7
|
const market = program.command("market").description("Market data commands");
|
|
7
8
|
market
|
|
8
9
|
.command("list")
|
|
9
10
|
.description("List all available markets")
|
|
10
11
|
.action(async () => {
|
|
12
|
+
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
13
|
+
// Multi-exchange mode
|
|
14
|
+
if (!explicitExchange && getAdapterForExchange) {
|
|
15
|
+
const allRows = [];
|
|
16
|
+
const errors = {};
|
|
17
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
18
|
+
try {
|
|
19
|
+
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
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
36
|
+
}
|
|
37
|
+
}));
|
|
38
|
+
// Sort by symbol name
|
|
39
|
+
allRows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
40
|
+
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
|
+
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
50
|
+
}
|
|
51
|
+
console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding", "24h Vol", "OI", "Max Lev"], allRows));
|
|
52
|
+
for (const [ex, msg] of Object.entries(errors)) {
|
|
53
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Single exchange mode
|
|
11
58
|
const adapter = await getAdapter();
|
|
12
59
|
const markets = await adapter.getMarkets();
|
|
13
60
|
if (isJson())
|
|
@@ -27,6 +74,45 @@ export function registerMarketCommands(program, getAdapter, isJson) {
|
|
|
27
74
|
.command("prices")
|
|
28
75
|
.description("Get current prices for all markets")
|
|
29
76
|
.action(async () => {
|
|
77
|
+
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
78
|
+
if (!explicitExchange && getAdapterForExchange) {
|
|
79
|
+
const allRows = [];
|
|
80
|
+
const errors = {};
|
|
81
|
+
await Promise.all(EXCHANGES.map(async (ex) => {
|
|
82
|
+
try {
|
|
83
|
+
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
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
97
|
+
}
|
|
98
|
+
}));
|
|
99
|
+
allRows.sort((a, b) => a[0].localeCompare(b[0]));
|
|
100
|
+
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
|
+
return printJson(jsonOk({ exchanges: grouped, errors: Object.keys(errors).length > 0 ? errors : undefined }));
|
|
110
|
+
}
|
|
111
|
+
console.log(makeTable(["Symbol", "Exch", "Mark", "Index", "Funding"], allRows));
|
|
112
|
+
for (const [ex, msg] of Object.entries(errors))
|
|
113
|
+
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
30
116
|
const adapter = await getAdapter();
|
|
31
117
|
if (adapter instanceof PacificaAdapter) {
|
|
32
118
|
const prices = await adapter.sdk.getPrices();
|
package/dist/index.js
CHANGED
|
@@ -188,7 +188,7 @@ function getHLAdapter() {
|
|
|
188
188
|
return _hlAdapter;
|
|
189
189
|
}
|
|
190
190
|
// Register command groups with async adapter getter
|
|
191
|
-
registerMarketCommands(program, getAdapter, isJson);
|
|
191
|
+
registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
192
192
|
registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
|
|
193
193
|
registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
|
|
194
194
|
registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
|
package/dist/mcp-server.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MCP (Model Context Protocol) server for perp-cli.
|
|
4
4
|
*
|
|
5
|
-
* Read-only advisor mode: provides market data, account
|
|
5
|
+
* Read-only advisor mode: provides market data, account balance, and CLI command suggestions.
|
|
6
6
|
* Does NOT execute trades directly — instead suggests CLI commands for the user to run.
|
|
7
7
|
* Adapters are created lazily from environment variables.
|
|
8
8
|
*/
|
package/dist/mcp-server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* MCP (Model Context Protocol) server for perp-cli.
|
|
4
4
|
*
|
|
5
|
-
* Read-only advisor mode: provides market data, account
|
|
5
|
+
* Read-only advisor mode: provides market data, account balance, and CLI command suggestions.
|
|
6
6
|
* Does NOT execute trades directly — instead suggests CLI commands for the user to run.
|
|
7
7
|
* Adapters are created lazily from environment variables.
|
|
8
8
|
*/
|
|
@@ -270,18 +270,18 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
270
270
|
if (g.includes("buy") || g.includes("long")) {
|
|
271
271
|
const symbol = extractSymbol(g) || "BTC";
|
|
272
272
|
const size = extractNumber(g) || "<size>";
|
|
273
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account
|
|
273
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} buy ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} buy ${size}`, description: `Buy ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
|
|
274
274
|
}
|
|
275
275
|
else if (g.includes("sell") || g.includes("short")) {
|
|
276
276
|
const symbol = extractSymbol(g) || "BTC";
|
|
277
277
|
const size = extractNumber(g) || "<size>";
|
|
278
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account
|
|
278
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} sell ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} sell ${size}`, description: `Sell ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
|
|
279
279
|
}
|
|
280
280
|
else if (g.includes("limit")) {
|
|
281
281
|
const symbol = extractSymbol(g) || "BTC";
|
|
282
282
|
const size = extractNumber(g) || "<size>";
|
|
283
283
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
284
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook for price levels` }, { step: 2, command: `perp -e ${ex} --json account
|
|
284
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook for price levels` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade limit ${symbol} ${side} <price> ${size}`, description: `Place limit ${side} order`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account orders`, description: "Verify order placed" });
|
|
285
285
|
}
|
|
286
286
|
else if (g.includes("close") || g.includes("exit") || g.includes("flatten")) {
|
|
287
287
|
const symbol = extractSymbol(g);
|
|
@@ -303,7 +303,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
303
303
|
else if (g.includes("scale-in") || g.includes("scale in") || g.includes("dca in")) {
|
|
304
304
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
305
305
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
306
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: "Check current prices" }, { step: 2, command: `perp -e ${ex} --json account
|
|
306
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: "Check current prices" }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade scale-in ${symbol} ${side} --levels '<price1>:<size>,<price2>:<size>'`, description: "Place scaled entry orders at multiple levels", dangerous: true });
|
|
307
307
|
}
|
|
308
308
|
else if (g.includes("tpsl") || g.includes("tp/sl") || g.includes("tp sl") || (g.includes("take profit") && g.includes("stop loss"))) {
|
|
309
309
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
@@ -322,13 +322,13 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
322
322
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
323
323
|
const amount = extractNumber(g) || "<usd>";
|
|
324
324
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
325
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json account
|
|
325
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade split ${symbol} ${side} ${amount}`, description: `Split ${side} $${amount} ${symbol} into depth-based slices`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
|
|
326
326
|
}
|
|
327
327
|
else if (g.includes("twap")) {
|
|
328
328
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
329
329
|
const size = extractNumber(g) || "<size>";
|
|
330
330
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
331
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json account
|
|
331
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json trade twap ${symbol} ${side} ${size} <duration>`, description: `TWAP ${side} ${size} ${symbol} over duration`, dangerous: true });
|
|
332
332
|
}
|
|
333
333
|
else if (g.includes("reduce")) {
|
|
334
334
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
@@ -362,7 +362,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
362
362
|
else if (g.includes("dca")) {
|
|
363
363
|
const amount = extractNumber(g) || "<amount>";
|
|
364
364
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
365
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json account
|
|
365
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json bot quick-dca ${symbol} ${side} ${amount} <interval>`, description: "Start DCA bot", dangerous: true }, { step: 3, command: "perp --json jobs list", description: "Verify bot is running" });
|
|
366
366
|
}
|
|
367
367
|
else {
|
|
368
368
|
steps.push({ step: 1, command: "perp --json bot preset-list", description: "List available bot presets" }, { step: 2, command: `perp -e ${ex} --json bot quick-arb`, description: "Start arb bot", dangerous: true }, { step: 3, command: "perp --json jobs list", description: "Verify bot is running" });
|
|
@@ -420,11 +420,11 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
420
420
|
}
|
|
421
421
|
else if (g.includes("deposit")) {
|
|
422
422
|
const amount = extractNumber(g) || "<amount>";
|
|
423
|
-
steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json account
|
|
423
|
+
steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json account balance`, description: "Verify deposit arrived" });
|
|
424
424
|
}
|
|
425
425
|
else if (g.includes("withdraw")) {
|
|
426
426
|
const amount = extractNumber(g) || "<amount>";
|
|
427
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json account
|
|
427
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json account balance`, description: "Check available balance" }, { step: 2, command: `perp --json funds withdraw ${ex} ${amount}`, description: `Withdraw $${amount} from ${ex}`, dangerous: true }, { step: 3, command: "perp --json wallet balance", description: "Verify withdrawal received" });
|
|
428
428
|
}
|
|
429
429
|
else if (g.includes("leverage")) {
|
|
430
430
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
@@ -523,7 +523,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
523
523
|
relatedCommands: [
|
|
524
524
|
`perp trade check ${symbol} ${side} ${sizeOrPrice || "<size>"}`,
|
|
525
525
|
`perp market book ${symbol}`,
|
|
526
|
-
`perp account
|
|
526
|
+
`perp account balance`,
|
|
527
527
|
],
|
|
528
528
|
};
|
|
529
529
|
}
|
|
@@ -596,7 +596,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
596
596
|
? ["Places multiple reduce-only limit orders", "Total percentage should not exceed 100%"]
|
|
597
597
|
: ["Places multiple limit orders — total size uses margin", "Review available balance before placing"],
|
|
598
598
|
category: "write",
|
|
599
|
-
relatedCommands: isTP ? ["perp account positions", "perp trade tpsl"] : ["perp account
|
|
599
|
+
relatedCommands: isTP ? ["perp account positions", "perp trade tpsl"] : ["perp account balance", "perp market book"],
|
|
600
600
|
};
|
|
601
601
|
}
|
|
602
602
|
else if (category === "trade" && sub === "twap") {
|
|
@@ -611,7 +611,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
611
611
|
],
|
|
612
612
|
risks: ["Executes real trades over time — total size will be filled", "Market may move during execution"],
|
|
613
613
|
category: "write",
|
|
614
|
-
relatedCommands: ["perp account
|
|
614
|
+
relatedCommands: ["perp account balance", "perp trade cancel-twap"],
|
|
615
615
|
};
|
|
616
616
|
}
|
|
617
617
|
else if (category === "trade" && (sub === "trailing-stop" || sub === "pnl-track")) {
|
|
@@ -761,7 +761,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
761
761
|
parameters: [{ name: "amount", value: args[3] || args[2], description: "Amount in USDC" }],
|
|
762
762
|
risks: ["Moves real funds — verify amount before executing"],
|
|
763
763
|
category: "write",
|
|
764
|
-
relatedCommands: ["perp wallet balance", "perp account
|
|
764
|
+
relatedCommands: ["perp wallet balance", "perp account balance"],
|
|
765
765
|
};
|
|
766
766
|
}
|
|
767
767
|
else if (category === "risk") {
|
|
@@ -971,7 +971,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
971
971
|
account: {
|
|
972
972
|
description: "Account data (read-only)",
|
|
973
973
|
subcommands: {
|
|
974
|
-
info: { usage: "perp account
|
|
974
|
+
info: { usage: "perp account balance", description: "Balance, equity, margin, PnL" },
|
|
975
975
|
positions: { usage: "perp account positions", description: "Open positions" },
|
|
976
976
|
orders: { usage: "perp account orders", description: "Open/pending orders" },
|
|
977
977
|
history: { usage: "perp account history", description: "Order history" },
|
package/package.json
CHANGED
|
@@ -17,7 +17,7 @@ perp --json wallet generate evm # generate EVM wallet
|
|
|
17
17
|
perp --json wallet generate solana # generate Solana wallet
|
|
18
18
|
perp --json wallet show # check configured wallets
|
|
19
19
|
perp --json wallet balance # on-chain USDC balances
|
|
20
|
-
perp --json -e <EX> account
|
|
20
|
+
perp --json -e <EX> account balance # exchange account balance
|
|
21
21
|
perp --json -e <EX> account positions # open positions
|
|
22
22
|
perp --json -e <EX> market list # available markets
|
|
23
23
|
perp --json -e <EX> trade market ... # execute trade
|
|
@@ -46,7 +46,7 @@ perp --json wallet show
|
|
|
46
46
|
# → check "ok": true and address appears
|
|
47
47
|
|
|
48
48
|
# 4. Check balance
|
|
49
|
-
perp --json -e hl account
|
|
49
|
+
perp --json -e hl account balance
|
|
50
50
|
# → if balance is 0, tell user to deposit USDC
|
|
51
51
|
|
|
52
52
|
# 5. Ready to trade
|
|
@@ -68,8 +68,8 @@ perp --json wallet show
|
|
|
68
68
|
# → should show both exchanges with addresses
|
|
69
69
|
|
|
70
70
|
# 3. Check balances on both
|
|
71
|
-
perp --json -e hl account
|
|
72
|
-
perp --json -e pac account
|
|
71
|
+
perp --json -e hl account balance
|
|
72
|
+
perp --json -e pac account balance
|
|
73
73
|
|
|
74
74
|
# 4. If one side needs funding, bridge USDC
|
|
75
75
|
perp --json bridge quote --from solana --to arbitrum --amount 500
|
|
@@ -115,7 +115,7 @@ Aliases for exchange names:
|
|
|
115
115
|
### Check On-Chain vs Exchange Balance
|
|
116
116
|
```bash
|
|
117
117
|
perp --json wallet balance # on-chain USDC in your wallet
|
|
118
|
-
perp --json -e hl account
|
|
118
|
+
perp --json -e hl account balance # USDC deposited on exchange
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
**On-chain balance ≠ exchange balance.** USDC in your wallet must be deposited to the exchange before trading.
|
|
@@ -237,7 +237,7 @@ Error case:
|
|
|
237
237
|
|
|
238
238
|
### Safe to retry (idempotent):
|
|
239
239
|
- `wallet show`, `wallet balance` — read-only
|
|
240
|
-
- `account
|
|
240
|
+
- `account balance`, `account positions`, `account orders` — read-only
|
|
241
241
|
- `market list`, `market mid`, `market book` — read-only
|
|
242
242
|
- `arb scan` — read-only (`arb rates` is deprecated)
|
|
243
243
|
- `portfolio`, `risk overview` — read-only
|
|
@@ -279,7 +279,7 @@ Symbols are auto-resolved by the CLI. **Always use bare symbols** (e.g., `BTC`,
|
|
|
279
279
|
|
|
280
280
|
1. **Using `perp init`** — interactive, will hang forever. Use `wallet set` instead.
|
|
281
281
|
2. **Forgetting `--json`** — output becomes unparseable human text.
|
|
282
|
-
3. **Trading with zero balance** — check `account
|
|
282
|
+
3. **Trading with zero balance** — check `account balance` first, tell user to deposit.
|
|
283
283
|
4. **Retrying a trade without checking** — leads to double positions. Always check `account positions` after a trade, even if it seemed to fail.
|
|
284
284
|
5. **Bridging without quoting** — always run `bridge quote` first to show the user fees and estimated time.
|
|
285
285
|
6. **Assuming deposit is instant** — after `bridge send`, wait for `bridge status` to confirm completion before depositing to the destination exchange.
|
|
@@ -17,7 +17,7 @@ perp --json market hip3 # list HIP-3 deployed perp dexes (Hyp
|
|
|
17
17
|
|
|
18
18
|
## Account (read-only, safe)
|
|
19
19
|
```bash
|
|
20
|
-
perp --json account balance # perp balance + spot holdings + 24h funding (alias: account
|
|
20
|
+
perp --json account balance # perp balance + spot holdings + 24h funding (alias: account balance)
|
|
21
21
|
perp --json account positions # open positions
|
|
22
22
|
perp --json account orders # open/pending orders
|
|
23
23
|
perp --json account history # order history
|
|
@@ -405,8 +405,8 @@ Rules of thumb:
|
|
|
405
405
|
|
|
406
406
|
Each exchange holds its own separate balance. Before entering any arb:
|
|
407
407
|
```bash
|
|
408
|
-
perp --json -e <EX_A> account
|
|
409
|
-
perp --json -e <EX_B> account
|
|
408
|
+
perp --json -e <EX_A> account balance # check available balance on exchange A
|
|
409
|
+
perp --json -e <EX_B> account balance # check available balance on exchange B
|
|
410
410
|
```
|
|
411
411
|
|
|
412
412
|
**Size each leg to fit the available balance on that exchange:**
|
|
@@ -53,8 +53,8 @@ else
|
|
|
53
53
|
fi
|
|
54
54
|
|
|
55
55
|
# 2. Balance check on both exchanges
|
|
56
|
-
LONG_BAL=$($PERP --json -e "$LONG_EX" account
|
|
57
|
-
SHORT_BAL=$($PERP --json -e "$SHORT_EX" account
|
|
56
|
+
LONG_BAL=$($PERP --json -e "$LONG_EX" account balance 2>/dev/null | jq -r '.data.available // .data.equity // "0"')
|
|
57
|
+
SHORT_BAL=$($PERP --json -e "$SHORT_EX" account balance 2>/dev/null | jq -r '.data.available // .data.equity // "0"')
|
|
58
58
|
|
|
59
59
|
NEEDED=$(echo "$SIZE" | awk '{print $1}')
|
|
60
60
|
if (( $(echo "$LONG_BAL < $NEEDED" | bc -l 2>/dev/null || echo 1) )); then
|