perp-cli 0.7.47 → 0.7.48

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.
@@ -334,7 +334,9 @@ export class FundingArbStrategy {
334
334
  const longAdapter = adapters.get(pos.longExchange);
335
335
  const shortAdapter = adapters.get(pos.shortExchange);
336
336
  if (longAdapter && shortAdapter) {
337
+ const isPositionGone = (msg) => msg.includes("ReduceOnly") || msg.includes("reduceOnly") || msg.includes("-2022");
337
338
  let closeLongOk = false;
339
+ let longPositionGone = false;
338
340
  try {
339
341
  const closeLongResult = await longAdapter.marketOrder(pos.symbol, "sell", pos.size, { reduceOnly: true });
340
342
  closeLongOk = true;
@@ -342,11 +344,18 @@ export class FundingArbStrategy {
342
344
  }
343
345
  catch (err) {
344
346
  const msg = err instanceof Error ? err.message : String(err);
345
- ctx.log(` [ARB] Close long leg failed for ${pos.symbol}: ${msg} (cooldown 5m)`);
346
- logExecution({ type: "arb_close", exchange: pos.longExchange, symbol: pos.symbol, side: "sell", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "long" } });
347
- this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
347
+ if (isPositionGone(msg)) {
348
+ // Position already liquidated/closed on this exchange
349
+ longPositionGone = true;
350
+ ctx.log(` [ARB] Long position gone for ${pos.symbol} on ${pos.longExchange} (liquidated?) — force closing short`);
351
+ }
352
+ else {
353
+ ctx.log(` [ARB] Close long leg failed for ${pos.symbol}: ${msg} (cooldown 5m)`);
354
+ logExecution({ type: "arb_close", exchange: pos.longExchange, symbol: pos.symbol, side: "sell", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "long" } });
355
+ this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
356
+ }
348
357
  }
349
- if (!closeLongOk)
358
+ if (!closeLongOk && !longPositionGone)
350
359
  continue;
351
360
  try {
352
361
  const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size, { reduceOnly: true });
@@ -354,25 +363,44 @@ export class FundingArbStrategy {
354
363
  }
355
364
  catch (err) {
356
365
  const msg = err instanceof Error ? err.message : String(err);
357
- ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
358
- logExecution({ type: "arb_close", exchange: pos.shortExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "short" } });
359
- // Rollback: re-open long to restore hedge
360
- try {
361
- await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
362
- ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
363
- // Note: re-open is NOT reduceOnly (it's a new position)
364
- logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
366
+ if (isPositionGone(msg)) {
367
+ // Short position also gone — both liquidated, just clean up tracking
368
+ ctx.log(` [ARB] Short position also gone for ${pos.symbol} on ${pos.shortExchange} — removing from tracking`);
365
369
  }
366
- catch (rbErr) {
367
- const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
368
- ctx.log(` [ARB] CRITICAL: Close rollback failed for ${pos.symbol}: ${rbMsg}`);
369
- logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
370
+ else if (closeLongOk) {
371
+ // Long was closed but short failed (not liquidated) — rollback long
372
+ ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
373
+ logExecution({ type: "arb_close", exchange: pos.shortExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: msg, dryRun: false, meta: { mode: pos.mode, leg: "short" } });
374
+ try {
375
+ await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
376
+ ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
377
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
378
+ }
379
+ catch (rbErr) {
380
+ const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
381
+ ctx.log(` [ARB] CRITICAL: Close rollback failed for ${pos.symbol}: ${rbMsg}`);
382
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
383
+ }
384
+ this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
385
+ continue;
386
+ }
387
+ else {
388
+ // Long was gone, short failed (not liquidated) — orphaned short, force close without reduceOnly
389
+ ctx.log(` [ARB] Force closing orphaned short for ${pos.symbol}: ${msg}`);
390
+ try {
391
+ await shortAdapter.marketOrder(pos.symbol, "buy", pos.size);
392
+ ctx.log(` [ARB] Force closed short ${pos.size} ${pos.symbol} on ${pos.shortExchange}`);
393
+ }
394
+ catch (forceErr) {
395
+ const forceMsg = forceErr instanceof Error ? forceErr.message : String(forceErr);
396
+ ctx.log(` [ARB] CRITICAL: Force close short failed for ${pos.symbol}: ${forceMsg}`);
397
+ this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 10 * 60 * 1000);
398
+ continue;
399
+ }
370
400
  }
371
- this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
372
- continue;
373
401
  }
374
402
  toClose.push(pos);
375
- ctx.log(` [ARB] Closed ${pos.symbol} position`);
403
+ ctx.log(` [ARB] Closed ${pos.symbol} position${longPositionGone ? ' (long was liquidated)' : ''}`);
376
404
  }
377
405
  }
378
406
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.47",
3
+ "version": "0.7.48",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",