perp-cli 0.7.48 → 0.7.50

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.
@@ -201,12 +201,21 @@ export class FundingArbStrategy {
201
201
  const equity = parseFloat(bal.equity);
202
202
  const marginUsed = parseFloat(bal.marginUsed);
203
203
  const marginPct = equity > 0 ? (marginUsed / equity) * 100 : 100;
204
- exchangeBalances.set(name, { equity, available: parseFloat(bal.available), marginPct });
205
- if (marginPct < 90) {
206
- availableAdapters.set(name, a);
204
+ const available = parseFloat(bal.available);
205
+ exchangeBalances.set(name, { equity, available, marginPct });
206
+ // Skip if: margin > 90%, available < $5, or exchange on liquidation cooldown
207
+ const exchCooldown = this._failCooldown.get(`${name}:exchange`) ?? 0;
208
+ if (Date.now() < exchCooldown) {
209
+ ctx.log(` [ARB] ${name} on liquidation cooldown — skipping`);
210
+ }
211
+ else if (available < 5) {
212
+ ctx.log(` [ARB] ${name} available $${available.toFixed(2)} too low — skipping`);
213
+ }
214
+ else if (marginPct >= 90) {
215
+ ctx.log(` [ARB] ${name} margin ${marginPct.toFixed(0)}% — skipping`);
207
216
  }
208
217
  else {
209
- ctx.log(` [ARB] ${name} margin ${marginPct.toFixed(0)}% — skipping for this tick`);
218
+ availableAdapters.set(name, a);
210
219
  }
211
220
  }
212
221
  catch (err) {
@@ -348,6 +357,8 @@ export class FundingArbStrategy {
348
357
  // Position already liquidated/closed on this exchange
349
358
  longPositionGone = true;
350
359
  ctx.log(` [ARB] Long position gone for ${pos.symbol} on ${pos.longExchange} (liquidated?) — force closing short`);
360
+ // Cooldown the exchange for 30 minutes to prevent liquidation-entry cycle
361
+ this._failCooldown.set(`${pos.longExchange}:exchange`, Date.now() + 30 * 60 * 1000);
351
362
  }
352
363
  else {
353
364
  ctx.log(` [ARB] Close long leg failed for ${pos.symbol}: ${msg} (cooldown 5m)`);
@@ -366,6 +377,7 @@ export class FundingArbStrategy {
366
377
  if (isPositionGone(msg)) {
367
378
  // Short position also gone — both liquidated, just clean up tracking
368
379
  ctx.log(` [ARB] Short position also gone for ${pos.symbol} on ${pos.shortExchange} — removing from tracking`);
380
+ this._failCooldown.set(`${pos.shortExchange}:exchange`, Date.now() + 30 * 60 * 1000);
369
381
  }
370
382
  else if (closeLongOk) {
371
383
  // Long was closed but short failed (not liquidated) — rollback long
@@ -623,20 +635,7 @@ export class FundingArbStrategy {
623
635
  const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
624
636
  ctx.log(` [ARB] CRITICAL: Rollback failed for ${best.symbol}: ${rbMsg}`);
625
637
  logExecution({ type: "multi_leg_rollback", exchange: best.longExchange, symbol: best.symbol, side: "sell", size: matched.size, status: "failed", error: rbMsg, dryRun: false });
626
- // Track orphaned long position for manual cleanup
627
- const currentPositions = ctx.state.get("arbOpenPositions");
628
- currentPositions.push({
629
- symbol: best.symbol,
630
- longExchange: best.longExchange,
631
- shortExchange: best.shortExchange, // note: short leg doesn't exist
632
- entrySpread: 0,
633
- size: matched.size,
634
- mode: "perp-perp",
635
- entryTime: 0, // flag as orphaned
636
- });
637
- ctx.state.set("arbOpenPositions", currentPositions);
638
- ctx.state.set("arbPositions", currentPositions.length);
639
- ctx.log(` [ARB] Tracking orphaned long position for manual cleanup`);
638
+ ctx.log(` [ARB] CRITICAL: Orphaned long position on ${best.longExchange} for ${best.symbol} — operator must close manually`);
640
639
  }
641
640
  this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
642
641
  continue;
@@ -979,6 +978,11 @@ export class FundingArbStrategy {
979
978
  const msg = spotErr instanceof Error ? spotErr.message : String(spotErr);
980
979
  ctx.log(` [ARB] Open spot-perp failed (spot buy) for ${opp.symbol}: ${msg}`);
981
980
  logExecution({ type: "arb_entry", exchange: opp.longExchange, symbol: opp.symbol, side: "buy", size: matched.size, status: "failed", error: msg, dryRun: false, meta: { mode: "spot-perp", leg: "spot" } });
981
+ try {
982
+ await this.transferUsdcToPerp(spotAdapter, exchangeName, transferAmt);
983
+ ctx.log(` [ARB] Transferred $${transferAmt} USDC back to perp after spot buy failure`);
984
+ }
985
+ catch { /* best effort */ }
982
986
  return false;
983
987
  }
984
988
  // Step 3: Short perp
@@ -1143,9 +1147,12 @@ export class FundingArbStrategy {
1143
1147
  // Bootstrap aster funding hours lazily (1 API call per unknown symbol, cached permanently)
1144
1148
  if (exchangeName === "aster" && "getFundingHours" in adapter) {
1145
1149
  const aster = adapter;
1146
- // Only bootstrap symbols not yet cached (up to 5 per tick to limit API calls)
1147
- const uncached = withRates.filter(m => m.fundingHours === undefined || m.fundingHours === 1);
1148
- const toBootstrap = uncached.slice(0, 5);
1150
+ // Only bootstrap symbols not yet cached (up to 20 per tick to complete faster)
1151
+ const uncached = withRates.filter(m => {
1152
+ const cached = adapter?._fundingHoursCache?.get?.(m.symbol);
1153
+ return cached === undefined; // only truly uncached, not symbols confirmed as 1h
1154
+ });
1155
+ const toBootstrap = uncached.slice(0, 20);
1149
1156
  for (const m of toBootstrap) {
1150
1157
  const fh = await aster.getFundingHours(m.symbol);
1151
1158
  m.fundingHours = fh;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.48",
3
+ "version": "0.7.50",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",