perp-cli 0.4.10 → 0.4.12
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 +162 -22
- package/dist/commands/agent.js +4 -4
- package/dist/commands/market.d.ts +1 -1
- package/dist/commands/market.js +87 -1
- package/dist/exchanges/lighter-spot.js +4 -19
- 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
|
@@ -146,10 +146,23 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
catch { /* spot not available */ }
|
|
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;
|
|
149
152
|
const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
|
|
150
153
|
const recent = fundingPayments.filter(f => f.time >= cutoff24h);
|
|
151
154
|
const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
|
|
152
155
|
const totalSpotUsd = spotBalances.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
|
|
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;
|
|
153
166
|
return {
|
|
154
167
|
exchange: name,
|
|
155
168
|
perp: bal,
|
|
@@ -157,19 +170,28 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
157
170
|
funding24h,
|
|
158
171
|
fundingPayments24h: recent.length,
|
|
159
172
|
totalSpotValueUsd: totalSpotUsd,
|
|
160
|
-
totalAccountValueUsd: Number(bal.equity) +
|
|
173
|
+
totalAccountValueUsd: Number(bal.equity) + nonUsdcSpotUsd,
|
|
174
|
+
unifiedAccount: isUnified,
|
|
161
175
|
};
|
|
162
176
|
}
|
|
163
177
|
function printSingleBalance(exName, r) {
|
|
164
|
-
console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance
|
|
165
|
-
|
|
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"));
|
|
166
184
|
console.log(` Equity: $${formatUsd(r.perp.equity)}`);
|
|
167
185
|
console.log(` Available: $${formatUsd(r.perp.available)}`);
|
|
168
186
|
console.log(` Margin Used: $${formatUsd(r.perp.marginUsed)}`);
|
|
169
187
|
console.log(` Unreal. PnL: ${formatPnl(r.perp.unrealizedPnl)}`);
|
|
170
|
-
|
|
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) {
|
|
171
193
|
console.log(chalk.white.bold("\n Spot Holdings"));
|
|
172
|
-
const rows =
|
|
194
|
+
const rows = displaySpot.map(b => [
|
|
173
195
|
chalk.white.bold(b.token),
|
|
174
196
|
b.total,
|
|
175
197
|
b.available,
|
|
@@ -181,16 +203,17 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
181
203
|
console.log(chalk.white.bold(" Funding (24h)"));
|
|
182
204
|
console.log(` Payments: ${r.fundingPayments24h}`);
|
|
183
205
|
console.log(` Total: ${formatPnl(r.funding24h)}`);
|
|
184
|
-
|
|
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) {
|
|
185
209
|
console.log(chalk.white.bold("\n Total"));
|
|
186
|
-
console.log(`
|
|
187
|
-
console.log(` Spot
|
|
210
|
+
console.log(` Equity: $${formatUsd(r.perp.equity)}`);
|
|
211
|
+
console.log(` Spot (other): $${formatUsd(nonUsdcSpotValue)}`);
|
|
188
212
|
console.log(` Total Value: $${formatUsd(r.totalAccountValueUsd)}`);
|
|
189
213
|
}
|
|
190
214
|
}
|
|
191
215
|
account
|
|
192
216
|
.command("balance")
|
|
193
|
-
.alias("info")
|
|
194
217
|
.description("Account overview: perp balance, spot holdings, and 24h funding")
|
|
195
218
|
.action(async () => {
|
|
196
219
|
// Detect if user explicitly set -e (single exchange mode)
|
|
@@ -223,17 +246,32 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
223
246
|
}
|
|
224
247
|
console.log(chalk.cyan.bold("\n All Exchanges — Account Balance\n"));
|
|
225
248
|
// Summary table
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
});
|
|
235
266
|
const grandEquity = results.reduce((s, r) => s + Number(r.perp.equity), 0);
|
|
236
|
-
const grandSpot = results.reduce((s, r) =>
|
|
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);
|
|
237
275
|
const grandFunding = results.reduce((s, r) => s + r.funding24h, 0);
|
|
238
276
|
const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
|
|
239
277
|
balRows.push([
|
|
@@ -246,11 +284,17 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
246
284
|
chalk.cyan.bold(`$${formatUsd(grandTotal)}`),
|
|
247
285
|
]);
|
|
248
286
|
console.log(makeTable(["Exchange", "Equity", "Available", "Margin", "Spot", "Funding 24h", "Total"], balRows));
|
|
249
|
-
|
|
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)
|
|
250
291
|
for (const r of results) {
|
|
251
|
-
|
|
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) {
|
|
252
296
|
console.log(chalk.white.bold(` ${r.exchange.toUpperCase()} Spot Holdings`));
|
|
253
|
-
const rows =
|
|
297
|
+
const rows = displaySpot.map(b => [
|
|
254
298
|
chalk.white.bold(b.token),
|
|
255
299
|
b.total,
|
|
256
300
|
b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
|
|
@@ -278,6 +322,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
278
322
|
.command("positions")
|
|
279
323
|
.description("Show open positions")
|
|
280
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
|
+
}
|
|
281
373
|
const adapter = await getAdapter();
|
|
282
374
|
const positions = await adapter.getPositions();
|
|
283
375
|
if (isJson())
|
|
@@ -302,6 +394,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
302
394
|
.command("orders")
|
|
303
395
|
.description("Show open orders")
|
|
304
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
|
+
}
|
|
305
445
|
const adapter = await getAdapter();
|
|
306
446
|
const orders = await adapter.getOpenOrders();
|
|
307
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();
|
|
@@ -140,30 +140,15 @@ export class LighterSpotAdapter {
|
|
|
140
140
|
?? res.accounts?.[0];
|
|
141
141
|
if (!acct)
|
|
142
142
|
return [];
|
|
143
|
-
//
|
|
144
|
-
|
|
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 (
|
|
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,
|
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
|