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.
@@ -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) => s + r.totalSpotValueUsd, 0),
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
- .action(async () => {
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
- if (isJson())
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
- if (positions.length === 0) {
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
- console.log(makeTable(["Symbol", "Side", "Size", "Entry", "Mark", "Liq", "PnL", "Lev"], rows));
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
- .action(async () => {
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
- if (isJson())
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
- if (orders.length === 0) {
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
- const rows = orders.map((o) => [
454
- o.orderId,
455
- chalk.white.bold(o.symbol),
456
- o.side === "buy" ? chalk.green("BUY") : chalk.red("SELL"),
457
- o.type,
458
- `$${formatUsd(o.price)}`,
459
- o.size,
460
- o.filled,
461
- o.status,
462
- ]);
463
- console.log(makeTable(["ID", "Symbol", "Side", "Type", "Price", "Size", "Filled", "Status"], rows));
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
  }
@@ -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-earned --json",
188
- "perp arb funding-earned --period 30 --json",
187
+ "perp arb status --funding --json",
188
+ "perp arb status --funding --period 30 --json",
189
189
  "perp arb history --json",
190
- "perp arb dex --json",
191
- "perp arb dex-monitor --min 10",
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 → funding-earned → close. Plus cross-dex arb and price gaps.",
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 (perp-perp + spot-perp) with PnL breakdown")
94
- .action(async () => {
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-earned ──
1193
- arb
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 (!isJson())
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 allPositions = [];
1368
+ const fundingPositions = [];
1205
1369
  for (const exName of EXCHANGES) {
1206
1370
  try {
1207
- const adapter = await getAdapterForExchange(exName);
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
- allPositions.push({
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 longs = allPositions.filter(p => p.side === "long");
1228
- const shorts = allPositions.filter(p => p.side === "short");
1229
- for (const l of longs) {
1230
- for (const s of shorts) {
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 getAdapterForExchange(exName);
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 (isJson()) {
1335
- return printJson(jsonOk({
1336
- pairs,
1337
- totals: { ...totals, annualizedApr: portfolioApr },
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 {};