perp-cli 0.4.12 → 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.
- package/dist/commands/account.js +151 -20
- package/dist/commands/agent.js +5 -5
- package/dist/commands/arb/index.js +191 -24
- package/dist/commands/arb-auto.d.ts +9 -0
- package/dist/commands/arb-auto.js +577 -382
- package/dist/commands/arb.d.ts +44 -0
- package/dist/commands/arb.js +236 -514
- package/dist/commands/market.js +108 -73
- package/dist/exchanges/hyperliquid.d.ts +2 -2
- package/dist/exchanges/hyperliquid.js +126 -31
- package/dist/exchanges/lighter.js +21 -7
- package/dist/liquidity.js +3 -1
- package/package.json +1 -1
package/dist/commands/account.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PacificaAdapter } from "../exchanges/pacifica.js";
|
|
2
|
+
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
2
3
|
import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMatch, withJsonErrors } from "../utils.js";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import { assessRisk } from "../risk.js";
|
|
@@ -239,7 +240,14 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
239
240
|
exchanges: results,
|
|
240
241
|
errors: Object.keys(errors).length > 0 ? errors : undefined,
|
|
241
242
|
totalEquity: results.reduce((s, r) => s + Number(r.perp.equity), 0),
|
|
242
|
-
totalSpotValueUsd: results.reduce((s, r) =>
|
|
243
|
+
totalSpotValueUsd: results.reduce((s, r) => {
|
|
244
|
+
if (r.unifiedAccount) {
|
|
245
|
+
return s + r.spot
|
|
246
|
+
.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
|
|
247
|
+
.reduce((ss, b) => ss + (b.valueUsd ?? 0), 0);
|
|
248
|
+
}
|
|
249
|
+
return s + r.totalSpotValueUsd;
|
|
250
|
+
}, 0),
|
|
243
251
|
totalAccountValueUsd: grandTotal,
|
|
244
252
|
totalFunding24h: results.reduce((s, r) => s + r.funding24h, 0),
|
|
245
253
|
}));
|
|
@@ -318,10 +326,26 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
318
326
|
printSingleBalance(exName, result);
|
|
319
327
|
console.log();
|
|
320
328
|
});
|
|
329
|
+
// ── HIP-3 dex helper: fetch data from all deployed dexes in parallel ──
|
|
330
|
+
async function fetchHip3Data(hlAdapter, fetcher) {
|
|
331
|
+
const dexes = await hlAdapter.listDeployedDexes();
|
|
332
|
+
const results = await Promise.allSettled(dexes.map(async (d) => {
|
|
333
|
+
// Clone adapter with dex set
|
|
334
|
+
const dexAdapter = Object.create(hlAdapter);
|
|
335
|
+
dexAdapter.setDex(d.name);
|
|
336
|
+
const items = await fetcher(dexAdapter);
|
|
337
|
+
return { dex: d.name, items };
|
|
338
|
+
}));
|
|
339
|
+
return results
|
|
340
|
+
.filter((r) => r.status === "fulfilled")
|
|
341
|
+
.map(r => r.value)
|
|
342
|
+
.filter(r => r.items.length > 0);
|
|
343
|
+
}
|
|
321
344
|
account
|
|
322
345
|
.command("positions")
|
|
323
346
|
.description("Show open positions")
|
|
324
|
-
.
|
|
347
|
+
.option("--hip3", "Include HIP-3 dex positions (Hyperliquid)")
|
|
348
|
+
.action(async (opts) => {
|
|
325
349
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
326
350
|
if (!explicitExchange && getAdapterForExchange) {
|
|
327
351
|
const allRows = [];
|
|
@@ -343,6 +367,25 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
343
367
|
`${p.leverage}x`,
|
|
344
368
|
]);
|
|
345
369
|
}
|
|
370
|
+
// --hip3: fetch HIP-3 dex positions for HL
|
|
371
|
+
if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
|
|
372
|
+
const hip3 = await fetchHip3Data(adapter, a => a.getPositions());
|
|
373
|
+
for (const { dex, items } of hip3) {
|
|
374
|
+
for (const p of items) {
|
|
375
|
+
allRows.push([
|
|
376
|
+
chalk.white.bold(p.symbol),
|
|
377
|
+
chalk.magenta(dex),
|
|
378
|
+
p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
|
|
379
|
+
p.size,
|
|
380
|
+
`$${formatUsd(p.entryPrice)}`,
|
|
381
|
+
`$${formatUsd(p.markPrice)}`,
|
|
382
|
+
p.liquidationPrice === "N/A" ? chalk.gray("N/A") : `$${formatUsd(p.liquidationPrice)}`,
|
|
383
|
+
formatPnl(p.unrealizedPnl),
|
|
384
|
+
`${p.leverage}x`,
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
346
389
|
}
|
|
347
390
|
catch (err) {
|
|
348
391
|
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
@@ -354,6 +397,11 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
354
397
|
try {
|
|
355
398
|
const adapter = await getAdapterForExchange(ex);
|
|
356
399
|
grouped[ex] = await adapter.getPositions();
|
|
400
|
+
if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
|
|
401
|
+
const hip3 = await fetchHip3Data(adapter, a => a.getPositions());
|
|
402
|
+
for (const { dex, items } of hip3)
|
|
403
|
+
grouped[`hip3:${dex}`] = items;
|
|
404
|
+
}
|
|
357
405
|
}
|
|
358
406
|
catch { /* skip */ }
|
|
359
407
|
}));
|
|
@@ -370,11 +418,24 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
370
418
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
371
419
|
return;
|
|
372
420
|
}
|
|
421
|
+
// Single exchange mode
|
|
373
422
|
const adapter = await getAdapter();
|
|
374
423
|
const positions = await adapter.getPositions();
|
|
375
|
-
|
|
424
|
+
// --hip3: append HIP-3 dex positions
|
|
425
|
+
const hip3Positions = [];
|
|
426
|
+
if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
|
|
427
|
+
hip3Positions.push(...await fetchHip3Data(adapter, a => a.getPositions()));
|
|
428
|
+
}
|
|
429
|
+
if (isJson()) {
|
|
430
|
+
if (hip3Positions.length > 0) {
|
|
431
|
+
const hip3Map = {};
|
|
432
|
+
for (const { dex, items } of hip3Positions)
|
|
433
|
+
hip3Map[dex] = items;
|
|
434
|
+
return printJson(jsonOk({ main: positions, hip3: hip3Map }));
|
|
435
|
+
}
|
|
376
436
|
return printJson(jsonOk(positions));
|
|
377
|
-
|
|
437
|
+
}
|
|
438
|
+
if (positions.length === 0 && hip3Positions.length === 0) {
|
|
378
439
|
console.log(chalk.gray("\n No open positions.\n"));
|
|
379
440
|
return;
|
|
380
441
|
}
|
|
@@ -388,12 +449,29 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
388
449
|
formatPnl(p.unrealizedPnl),
|
|
389
450
|
`${p.leverage}x`,
|
|
390
451
|
]);
|
|
391
|
-
|
|
452
|
+
if (rows.length > 0) {
|
|
453
|
+
console.log(makeTable(["Symbol", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], rows));
|
|
454
|
+
}
|
|
455
|
+
for (const { dex, items } of hip3Positions) {
|
|
456
|
+
console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`));
|
|
457
|
+
const dexRows = items.map((p) => [
|
|
458
|
+
chalk.white.bold(p.symbol),
|
|
459
|
+
p.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
|
|
460
|
+
p.size,
|
|
461
|
+
`$${formatUsd(p.entryPrice)}`,
|
|
462
|
+
`$${formatUsd(p.markPrice)}`,
|
|
463
|
+
p.liquidationPrice === "N/A" ? chalk.gray("N/A") : `$${formatUsd(p.liquidationPrice)}`,
|
|
464
|
+
formatPnl(p.unrealizedPnl),
|
|
465
|
+
`${p.leverage}x`,
|
|
466
|
+
]);
|
|
467
|
+
console.log(makeTable(["Symbol", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], dexRows));
|
|
468
|
+
}
|
|
392
469
|
});
|
|
393
470
|
account
|
|
394
471
|
.command("orders")
|
|
395
472
|
.description("Show open orders")
|
|
396
|
-
.
|
|
473
|
+
.option("--hip3", "Include HIP-3 dex orders (Hyperliquid)")
|
|
474
|
+
.action(async (opts) => {
|
|
397
475
|
const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
|
|
398
476
|
if (!explicitExchange && getAdapterForExchange) {
|
|
399
477
|
const allRows = [];
|
|
@@ -415,6 +493,25 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
415
493
|
o.status,
|
|
416
494
|
]);
|
|
417
495
|
}
|
|
496
|
+
// --hip3: fetch HIP-3 dex orders for HL
|
|
497
|
+
if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
|
|
498
|
+
const hip3 = await fetchHip3Data(adapter, a => a.getOpenOrders());
|
|
499
|
+
for (const { dex, items } of hip3) {
|
|
500
|
+
for (const o of items) {
|
|
501
|
+
allRows.push([
|
|
502
|
+
o.orderId,
|
|
503
|
+
chalk.white.bold(o.symbol),
|
|
504
|
+
chalk.magenta(dex),
|
|
505
|
+
o.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
|
|
506
|
+
o.type,
|
|
507
|
+
`$${formatUsd(o.price)}`,
|
|
508
|
+
o.size,
|
|
509
|
+
o.filled,
|
|
510
|
+
o.status,
|
|
511
|
+
]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
418
515
|
}
|
|
419
516
|
catch (err) {
|
|
420
517
|
errors[ex] = err instanceof Error ? err.message : String(err);
|
|
@@ -426,6 +523,11 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
426
523
|
try {
|
|
427
524
|
const adapter = await getAdapterForExchange(ex);
|
|
428
525
|
grouped[ex] = await adapter.getOpenOrders();
|
|
526
|
+
if (opts.hip3 && ex === "hyperliquid" && adapter instanceof HyperliquidAdapter) {
|
|
527
|
+
const hip3 = await fetchHip3Data(adapter, a => a.getOpenOrders());
|
|
528
|
+
for (const { dex, items } of hip3)
|
|
529
|
+
grouped[`hip3:${dex}`] = items;
|
|
530
|
+
}
|
|
429
531
|
}
|
|
430
532
|
catch { /* skip */ }
|
|
431
533
|
}));
|
|
@@ -442,25 +544,54 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
442
544
|
console.log(chalk.gray(` ${ex}: ${msg}`));
|
|
443
545
|
return;
|
|
444
546
|
}
|
|
547
|
+
// Single exchange mode
|
|
445
548
|
const adapter = await getAdapter();
|
|
446
549
|
const orders = await adapter.getOpenOrders();
|
|
447
|
-
|
|
550
|
+
// --hip3: append HIP-3 dex orders
|
|
551
|
+
const hip3Orders = [];
|
|
552
|
+
if (opts.hip3 && adapter instanceof HyperliquidAdapter) {
|
|
553
|
+
hip3Orders.push(...await fetchHip3Data(adapter, a => a.getOpenOrders()));
|
|
554
|
+
}
|
|
555
|
+
if (isJson()) {
|
|
556
|
+
if (hip3Orders.length > 0) {
|
|
557
|
+
const hip3Map = {};
|
|
558
|
+
for (const { dex, items } of hip3Orders)
|
|
559
|
+
hip3Map[dex] = items;
|
|
560
|
+
return printJson(jsonOk({ main: orders, hip3: hip3Map }));
|
|
561
|
+
}
|
|
448
562
|
return printJson(jsonOk(orders));
|
|
449
|
-
|
|
563
|
+
}
|
|
564
|
+
if (orders.length === 0 && hip3Orders.length === 0) {
|
|
450
565
|
console.log(chalk.gray("\n No open orders.\n"));
|
|
451
566
|
return;
|
|
452
567
|
}
|
|
453
|
-
|
|
454
|
-
o
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
568
|
+
if (orders.length > 0) {
|
|
569
|
+
const rows = orders.map((o) => [
|
|
570
|
+
o.orderId,
|
|
571
|
+
chalk.white.bold(o.symbol),
|
|
572
|
+
o.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
|
|
573
|
+
o.type,
|
|
574
|
+
`$${formatUsd(o.price)}`,
|
|
575
|
+
o.size,
|
|
576
|
+
o.filled,
|
|
577
|
+
o.status,
|
|
578
|
+
]);
|
|
579
|
+
console.log(makeTable(["ID", "Symbol", "Side", "Type", "Price", "Size", "Filled", "Status"], rows));
|
|
580
|
+
}
|
|
581
|
+
for (const { dex, items } of hip3Orders) {
|
|
582
|
+
console.log(chalk.magenta.bold(`\n HIP-3: ${dex}`));
|
|
583
|
+
const dexRows = items.map((o) => [
|
|
584
|
+
o.orderId,
|
|
585
|
+
chalk.white.bold(o.symbol),
|
|
586
|
+
o.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
|
|
587
|
+
o.type,
|
|
588
|
+
`$${formatUsd(o.price)}`,
|
|
589
|
+
o.size,
|
|
590
|
+
o.filled,
|
|
591
|
+
o.status,
|
|
592
|
+
]);
|
|
593
|
+
console.log(makeTable(["ID", "Symbol", "Side", "Type", "Price", "Size", "Filled", "Status"], dexRows));
|
|
594
|
+
}
|
|
464
595
|
});
|
|
465
596
|
account
|
|
466
597
|
.command("history")
|
|
@@ -495,7 +626,7 @@ export function registerAccountCommands(program, getAdapter, isJson, getAdapterF
|
|
|
495
626
|
const settings = await p.sdk.getAccountSettings(p.publicKey);
|
|
496
627
|
if (isJson())
|
|
497
628
|
return printJson(jsonOk(settings));
|
|
498
|
-
if (settings.length === 0) {
|
|
629
|
+
if (!Array.isArray(settings) || settings.length === 0) {
|
|
499
630
|
console.log(chalk.gray("\n No market settings configured.\n"));
|
|
500
631
|
return;
|
|
501
632
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -184,14 +184,14 @@ export function registerAgentCommands(program, getAdapter, isJson) {
|
|
|
184
184
|
"perp arb close <SYM> --json",
|
|
185
185
|
"perp arb close <SYM> --dry-run --json",
|
|
186
186
|
"perp arb close <SYM> --pair <longEx>:<shortEx> --json",
|
|
187
|
-
"perp arb funding
|
|
188
|
-
"perp arb funding
|
|
187
|
+
"perp arb status --funding --json",
|
|
188
|
+
"perp arb status --funding --period 30 --json",
|
|
189
189
|
"perp arb history --json",
|
|
190
|
-
"perp arb
|
|
191
|
-
"perp arb
|
|
190
|
+
"perp arb scan --hip3 --json",
|
|
191
|
+
"perp arb monitor --hip3 --min 10",
|
|
192
192
|
"perp gap show --json",
|
|
193
193
|
],
|
|
194
|
-
description: "Cross-exchange funding arb: scan → exec → status →
|
|
194
|
+
description: "Cross-exchange funding arb: scan → exec → status → close. Plus cross-dex arb (--hip3) and price gaps.",
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
category: "risk_management",
|
|
@@ -5,7 +5,7 @@ import { toHourlyRate, computeAnnualSpread } from "../../funding.js";
|
|
|
5
5
|
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
|
|
6
6
|
import { computeBasisRisk } from "../../arb-utils.js";
|
|
7
7
|
import { smartOrder } from "../../smart-order.js";
|
|
8
|
-
import { removePosition as persistRemovePosition, getPositions as getArbStatePositions } from "../../arb-state.js";
|
|
8
|
+
import { removePosition as persistRemovePosition, getPositions as getArbStatePositions, loadArbState } from "../../arb-state.js";
|
|
9
9
|
import { SPOT_PERP_TOKEN_MAP } from "../../exchanges/spot-interface.js";
|
|
10
10
|
import { fetchAllBalances, computeRebalancePlan } from "../../rebalance.js";
|
|
11
11
|
import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
|
|
@@ -90,8 +90,10 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
90
90
|
// ── arb status ──
|
|
91
91
|
arb
|
|
92
92
|
.command("status")
|
|
93
|
-
.description("Show open arb positions
|
|
94
|
-
.
|
|
93
|
+
.description("Show open arb positions with PnL breakdown + actual funding earned")
|
|
94
|
+
.option("--period <days>", "Funding lookback period in days", "7")
|
|
95
|
+
.option("--symbol <symbol>", "Filter funding data by symbol")
|
|
96
|
+
.action(async (opts) => {
|
|
95
97
|
if (!isJson())
|
|
96
98
|
console.log(chalk.cyan("\n Checking arb positions across exchanges...\n"));
|
|
97
99
|
// Fetch positions from all exchanges
|
|
@@ -482,6 +484,8 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
482
484
|
console.log(` Portfolio APR: ${chalk.cyan(`${portfolioApr.toFixed(1)}%`)}`);
|
|
483
485
|
}
|
|
484
486
|
console.log(chalk.gray(` (Fees assume ${(TAKER_FEE * 100).toFixed(3)}% taker for entry + exit.)\n`));
|
|
487
|
+
// Always show actual funding payments earned
|
|
488
|
+
await printFundingEarned(getAdapterForExchange, isJson, { period: opts.period, symbol: opts.symbol });
|
|
485
489
|
});
|
|
486
490
|
// ── arb close ──
|
|
487
491
|
arb
|
|
@@ -490,9 +494,174 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
490
494
|
.option("--dry-run", "Show what would happen without executing")
|
|
491
495
|
.option("--pair <pair>", "Specify arb pair as longExchange:shortExchange (e.g. pacifica:hyperliquid)")
|
|
492
496
|
.option("--smart", "Smart execution: IOC limit at best bid/ask + 1 tick (reduces slippage)")
|
|
497
|
+
.option("--spot-exch <exchange>", "Override spot exchange for spot-perp close (hl or lt)")
|
|
498
|
+
.option("--perp-exch <exchange>", "Override perp exchange for spot-perp close")
|
|
493
499
|
.action(async (symbol, opts) => {
|
|
494
500
|
const sym = symbol.toUpperCase();
|
|
495
501
|
const dryRun = !!opts.dryRun || process.argv.includes("--dry-run");
|
|
502
|
+
// Check if this is a spot-perp position (auto-detect from arb state)
|
|
503
|
+
const arbState = loadArbState();
|
|
504
|
+
const statePos = arbState?.positions?.find(p => p.symbol.toUpperCase() === sym && p.mode === "spot-perp");
|
|
505
|
+
if (statePos) {
|
|
506
|
+
// ── Inline spot-perp close logic ──
|
|
507
|
+
const aliasMap = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
|
|
508
|
+
const spotExch = aliasMap[opts.spotExch?.toLowerCase() ?? ""] || opts.spotExch?.toLowerCase() || statePos.spotExchange || statePos.longExchange;
|
|
509
|
+
const perpExch = aliasMap[opts.perpExch?.toLowerCase() ?? ""] || opts.perpExch?.toLowerCase() || statePos.shortExchange;
|
|
510
|
+
if (!spotExch || !perpExch) {
|
|
511
|
+
const msg = "Cannot determine exchanges. Specify --spot-exch and --perp-exch, or ensure the position is tracked in state.";
|
|
512
|
+
if (isJson())
|
|
513
|
+
return printJson(jsonOk({ error: msg }));
|
|
514
|
+
console.log(chalk.red(` ${msg}`));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
// Load spot adapter
|
|
518
|
+
const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
|
|
519
|
+
const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
|
|
520
|
+
let spotAdapter;
|
|
521
|
+
if (spotExch === "hyperliquid") {
|
|
522
|
+
const hlAdapter = await getAdapterForExchange("hyperliquid");
|
|
523
|
+
const hlSpot = new HyperliquidSpotAdapter(hlAdapter);
|
|
524
|
+
await hlSpot.init();
|
|
525
|
+
spotAdapter = hlSpot;
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const ltAdapter = await getAdapterForExchange("lighter");
|
|
529
|
+
const ltSpot = new LighterSpotAdapter(ltAdapter);
|
|
530
|
+
await ltSpot.init();
|
|
531
|
+
spotAdapter = ltSpot;
|
|
532
|
+
}
|
|
533
|
+
const perpAdapter = await getAdapterForExchange(perpExch);
|
|
534
|
+
const spotSymbol = `${sym}/USDC`;
|
|
535
|
+
// Find how much to close
|
|
536
|
+
const perpPositions = await perpAdapter.getPositions();
|
|
537
|
+
const perpPos = perpPositions.find(p => p.symbol.replace("-PERP", "").toUpperCase() === sym && p.side === "short");
|
|
538
|
+
if (!perpPos) {
|
|
539
|
+
const msg = `No short perp position found for ${sym} on ${perpExch}`;
|
|
540
|
+
if (isJson())
|
|
541
|
+
return printJson(jsonOk({ error: msg }));
|
|
542
|
+
console.log(chalk.red(` ${msg}`));
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const closeSize = perpPos.size;
|
|
546
|
+
if (!isJson()) {
|
|
547
|
+
console.log(chalk.cyan(`\n Closing Spot+Perp Arb: ${sym}`));
|
|
548
|
+
console.log(chalk.gray(` SPOT ${spotExch}: sell ${closeSize}`));
|
|
549
|
+
console.log(chalk.gray(` PERP ${perpExch}: buy ${closeSize} (close short)`));
|
|
550
|
+
console.log(chalk.gray(` Executing...\n`));
|
|
551
|
+
}
|
|
552
|
+
// Same exchange -> sequential (nonce collision), different -> parallel
|
|
553
|
+
let spotResult;
|
|
554
|
+
let perpResult;
|
|
555
|
+
if (spotExch === perpExch) {
|
|
556
|
+
spotResult = await spotAdapter.spotMarketOrder(spotSymbol, "sell", closeSize)
|
|
557
|
+
.then(r => ({ status: "fulfilled", value: r }))
|
|
558
|
+
.catch(e => ({ status: "rejected", reason: e }));
|
|
559
|
+
perpResult = await perpAdapter.marketOrder(sym, "buy", closeSize)
|
|
560
|
+
.then(r => ({ status: "fulfilled", value: r }))
|
|
561
|
+
.catch(e => ({ status: "rejected", reason: e }));
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
[spotResult, perpResult] = await Promise.allSettled([
|
|
565
|
+
spotAdapter.spotMarketOrder(spotSymbol, "sell", closeSize),
|
|
566
|
+
perpAdapter.marketOrder(sym, "buy", closeSize),
|
|
567
|
+
]);
|
|
568
|
+
}
|
|
569
|
+
const spotOk = spotResult.status === "fulfilled";
|
|
570
|
+
const perpOk = perpResult.status === "fulfilled";
|
|
571
|
+
if (spotOk && perpOk) {
|
|
572
|
+
// Post-close fill verification
|
|
573
|
+
let closeVerified = true;
|
|
574
|
+
const closeWarnings = [];
|
|
575
|
+
try {
|
|
576
|
+
await new Promise(r => setTimeout(r, 800));
|
|
577
|
+
const spotBals = await spotAdapter.getSpotBalances();
|
|
578
|
+
const spotTokenBal = spotBals.find(b => b.token.toUpperCase().startsWith(sym));
|
|
579
|
+
const remainingSpotAmt = Number(spotTokenBal?.total ?? 0);
|
|
580
|
+
const closedSize = parseFloat(closeSize);
|
|
581
|
+
if (remainingSpotAmt > closedSize * 0.5) {
|
|
582
|
+
closeVerified = false;
|
|
583
|
+
closeWarnings.push(`Spot sell may not have filled: still have ${remainingSpotAmt.toFixed(6)} ${sym} (expected ~0)`);
|
|
584
|
+
}
|
|
585
|
+
const perpPositionsAfter = await perpAdapter.getPositions();
|
|
586
|
+
const remainingPerpPos = perpPositionsAfter.find(p => p.symbol.replace("-PERP", "").toUpperCase() === sym && p.side === "short");
|
|
587
|
+
if (remainingPerpPos && Number(remainingPerpPos.size) > closedSize * 0.5) {
|
|
588
|
+
closeVerified = false;
|
|
589
|
+
closeWarnings.push(`Perp short still open: ${remainingPerpPos.size} ${sym} on ${perpExch} (expected closed)`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (verifyErr) {
|
|
593
|
+
closeWarnings.push(`Close verification failed: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`);
|
|
594
|
+
}
|
|
595
|
+
if (closeWarnings.length > 0 && !isJson()) {
|
|
596
|
+
for (const w of closeWarnings)
|
|
597
|
+
console.log(chalk.yellow(` Warning: ${w}`));
|
|
598
|
+
}
|
|
599
|
+
persistRemovePosition(sym);
|
|
600
|
+
logExecution({
|
|
601
|
+
type: "arb_close", exchange: `spot:${spotExch}+${perpExch}`,
|
|
602
|
+
symbol: sym, side: "exit", size: closeSize,
|
|
603
|
+
status: closeVerified ? "success" : "unverified", dryRun: false,
|
|
604
|
+
meta: { mode: "spot-perp", ...(closeWarnings.length > 0 && { warnings: closeWarnings }) },
|
|
605
|
+
});
|
|
606
|
+
// Return spot USDC proceeds back to perp account
|
|
607
|
+
if (spotExch === "lighter") {
|
|
608
|
+
try {
|
|
609
|
+
const ltSpot = spotAdapter;
|
|
610
|
+
const bals = await ltSpot.getSpotBalances();
|
|
611
|
+
const usdcBal = bals.find(b => b.token === "USDC");
|
|
612
|
+
const leftover = Number(usdcBal?.available ?? 0);
|
|
613
|
+
if (leftover > 0.01) {
|
|
614
|
+
await ltSpot.transferUsdcToPerp(leftover);
|
|
615
|
+
if (!isJson())
|
|
616
|
+
console.log(chalk.gray(` Returned ~$${leftover.toFixed(2)} USDC spot->perp on Lighter`));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch { /* non-critical */ }
|
|
620
|
+
}
|
|
621
|
+
else if (spotExch === "hyperliquid") {
|
|
622
|
+
try {
|
|
623
|
+
const hlSpot = spotAdapter;
|
|
624
|
+
const bals = await hlSpot.getSpotBalances();
|
|
625
|
+
const usdcBal = bals.find(b => b.token.startsWith("USDC"));
|
|
626
|
+
const leftover = Number(usdcBal?.available ?? 0);
|
|
627
|
+
if (leftover > 1)
|
|
628
|
+
await hlSpot.transferUsdcToPerp(Math.floor(leftover));
|
|
629
|
+
}
|
|
630
|
+
catch { /* non-critical */ }
|
|
631
|
+
}
|
|
632
|
+
const closeResult = {
|
|
633
|
+
status: closeVerified ? "closed" : "closed_unverified",
|
|
634
|
+
mode: "spot-perp", symbol: sym, size: closeSize,
|
|
635
|
+
...(closeWarnings.length > 0 && { warnings: closeWarnings }),
|
|
636
|
+
};
|
|
637
|
+
if (isJson())
|
|
638
|
+
return printJson(jsonOk(closeResult));
|
|
639
|
+
if (closeVerified) {
|
|
640
|
+
console.log(chalk.green(` CLOSED (verified): ${sym} ${closeSize} units`));
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
console.log(chalk.yellow(` CLOSED (unverified): ${sym} ${closeSize} units -- check positions manually`));
|
|
644
|
+
}
|
|
645
|
+
console.log(chalk.green(` SPOT ${spotExch}: sold`));
|
|
646
|
+
console.log(chalk.green(` PERP ${perpExch}: bought (closed short)\n`));
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const errors = [];
|
|
650
|
+
if (!spotOk)
|
|
651
|
+
errors.push(`Spot sell failed: ${spotResult.reason}`);
|
|
652
|
+
if (!perpOk)
|
|
653
|
+
errors.push(`Perp buy failed: ${perpResult.reason}`);
|
|
654
|
+
const result = { status: "partial_close", symbol: sym, spotOk, perpOk, errors };
|
|
655
|
+
if (isJson())
|
|
656
|
+
return printJson(jsonOk(result));
|
|
657
|
+
console.log(chalk.red(` Close partially failed:`));
|
|
658
|
+
for (const e of errors)
|
|
659
|
+
console.log(chalk.red(` ${e}`));
|
|
660
|
+
console.log(chalk.yellow(` Manual intervention may be needed.\n`));
|
|
661
|
+
}
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
// ── Perp-perp close logic (original) ──
|
|
496
665
|
if (!isJson()) {
|
|
497
666
|
console.log(chalk.cyan(`\n Closing arb position for ${sym}...\n`));
|
|
498
667
|
if (dryRun)
|
|
@@ -1189,26 +1358,21 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1189
1358
|
console.log(chalk.yellow("\n To execute, use: perp rebalance execute --auto-bridge\n"));
|
|
1190
1359
|
}
|
|
1191
1360
|
});
|
|
1192
|
-
// ── arb funding
|
|
1193
|
-
|
|
1194
|
-
.command("funding-earned")
|
|
1195
|
-
.description("Actual funding payments received/paid for arb positions")
|
|
1196
|
-
.option("--period <days>", "Number of days to look back", "7")
|
|
1197
|
-
.option("--symbol <symbol>", "Filter by symbol")
|
|
1198
|
-
.action(async (opts) => {
|
|
1361
|
+
// ── printFundingEarned helper (used by arb status --funding) ──
|
|
1362
|
+
async function printFundingEarned(getAdapter, isJsonFn, opts) {
|
|
1199
1363
|
const periodDays = parseInt(opts.period);
|
|
1200
1364
|
const filterSym = opts.symbol?.toUpperCase();
|
|
1201
|
-
if (!
|
|
1365
|
+
if (!isJsonFn())
|
|
1202
1366
|
console.log(chalk.cyan(`\n Fetching funding payments (last ${periodDays} days)...\n`));
|
|
1203
1367
|
// Fetch positions to identify arb pairs
|
|
1204
|
-
const
|
|
1368
|
+
const fundingPositions = [];
|
|
1205
1369
|
for (const exName of EXCHANGES) {
|
|
1206
1370
|
try {
|
|
1207
|
-
const adapter = await
|
|
1371
|
+
const adapter = await getAdapter(exName);
|
|
1208
1372
|
const positions = await adapter.getPositions();
|
|
1209
1373
|
for (const p of positions) {
|
|
1210
1374
|
const normalized = p.symbol.replace("-PERP", "").toUpperCase();
|
|
1211
|
-
|
|
1375
|
+
fundingPositions.push({
|
|
1212
1376
|
exchange: exName,
|
|
1213
1377
|
symbol: normalized,
|
|
1214
1378
|
side: p.side,
|
|
@@ -1224,10 +1388,10 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1224
1388
|
// Identify arb pairs (long on one exchange, short on another)
|
|
1225
1389
|
const arbPairKeys = new Set();
|
|
1226
1390
|
const arbPairInfo = new Map();
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1229
|
-
for (const l of
|
|
1230
|
-
for (const s of
|
|
1391
|
+
const fLongs = fundingPositions.filter(p => p.side === "long");
|
|
1392
|
+
const fShorts = fundingPositions.filter(p => p.side === "short");
|
|
1393
|
+
for (const l of fLongs) {
|
|
1394
|
+
for (const s of fShorts) {
|
|
1231
1395
|
if (l.exchange !== s.exchange && l.symbol === s.symbol) {
|
|
1232
1396
|
const key = l.symbol;
|
|
1233
1397
|
arbPairKeys.add(key);
|
|
@@ -1243,7 +1407,7 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1243
1407
|
const cutoff = Date.now() - periodDays * 24 * 60 * 60 * 1000;
|
|
1244
1408
|
for (const exName of EXCHANGES) {
|
|
1245
1409
|
try {
|
|
1246
|
-
const adapter = await
|
|
1410
|
+
const adapter = await getAdapter(exName);
|
|
1247
1411
|
const payments = await adapter.getFundingPayments(200);
|
|
1248
1412
|
for (const p of payments) {
|
|
1249
1413
|
if (p.time < cutoff)
|
|
@@ -1331,11 +1495,14 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1331
1495
|
const portfolioApr = totalArbNotional > 0
|
|
1332
1496
|
? (totals.dailyAvg * 365 / totalArbNotional) * 100
|
|
1333
1497
|
: 0;
|
|
1334
|
-
if (
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1498
|
+
if (isJsonFn()) {
|
|
1499
|
+
printJson(jsonOk({
|
|
1500
|
+
fundingEarned: {
|
|
1501
|
+
pairs,
|
|
1502
|
+
totals: { ...totals, annualizedApr: portfolioApr },
|
|
1503
|
+
},
|
|
1338
1504
|
}));
|
|
1505
|
+
return;
|
|
1339
1506
|
}
|
|
1340
1507
|
if (pairs.length === 0) {
|
|
1341
1508
|
console.log(chalk.gray(" No funding payments found.\n"));
|
|
@@ -1368,5 +1535,5 @@ export function registerArbManageCommands(program, getAdapterForExchange, isJson
|
|
|
1368
1535
|
console.log(` Portfolio APR: ${chalk.cyan(`${portfolioApr.toFixed(1)}%`)} (on $${formatUsd(totalArbNotional)} arb notional)`);
|
|
1369
1536
|
}
|
|
1370
1537
|
console.log();
|
|
1371
|
-
}
|
|
1538
|
+
}
|
|
1372
1539
|
}
|
|
@@ -50,5 +50,14 @@ export declare function isNearSettlement(longExchange: string, shortExchange: st
|
|
|
50
50
|
* meaning we're now paying on both sides instead of collecting.
|
|
51
51
|
*/
|
|
52
52
|
export declare function isSpreadReversed(longExchange: string, shortExchange: string, snapshot: FundingSnapshot): boolean;
|
|
53
|
+
export declare let handleSpotClose: ((sym: string, opts: {
|
|
54
|
+
spotExch?: string;
|
|
55
|
+
perpExch?: string;
|
|
56
|
+
}, getAdapterForExchange: (exchange: string) => Promise<import("../exchanges/interface.js").ExchangeAdapter>, isJson: () => boolean) => Promise<void>) | undefined;
|
|
53
57
|
export declare function registerArbAutoCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean, getHLAdapterForDex?: (dex: string) => Promise<import("../exchanges/hyperliquid.js").HyperliquidAdapter>): void;
|
|
58
|
+
export declare let handleArbPnlRef: (() => Promise<void>) | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Exported wrapper for handleArbPnl.
|
|
61
|
+
*/
|
|
62
|
+
export declare function handleArbPnl(getAdapterForExchange: (exchange: string) => Promise<import("../exchanges/interface.js").ExchangeAdapter>, isJson: () => boolean): Promise<void>;
|
|
54
63
|
export {};
|