perp-cli 0.7.30 → 0.7.32

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.
@@ -293,19 +293,32 @@ export class FundingArbStrategy {
293
293
  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" } });
294
294
  this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
295
295
  }
296
+ if (!closeLongOk)
297
+ continue; // don't close short if long failed — keep hedge intact
296
298
  try {
297
299
  const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size);
298
300
  logExecution({ type: "arb_close", exchange: pos.shortExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false, meta: { mode: pos.mode, leg: "short", signedSpread: signedSpread, response: closeShortResult } });
299
301
  }
300
302
  catch (err) {
301
303
  const msg = err instanceof Error ? err.message : String(err);
302
- ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg}`);
304
+ ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
303
305
  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" } });
306
+ // Rollback: re-open long to restore hedge
307
+ try {
308
+ await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
309
+ ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
310
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
311
+ }
312
+ catch (rbErr) {
313
+ const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
314
+ ctx.log(` [ARB] CRITICAL: Close rollback failed for ${pos.symbol}: ${rbMsg}`);
315
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
316
+ }
317
+ this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
318
+ continue;
304
319
  }
305
- if (closeLongOk) {
306
- toClose.push(pos);
307
- ctx.log(` [ARB] Closed ${pos.symbol} position`);
308
- }
320
+ toClose.push(pos);
321
+ ctx.log(` [ARB] Closed ${pos.symbol} position`);
309
322
  }
310
323
  }
311
324
  }
@@ -277,17 +277,51 @@ export class AsterAdapter {
277
277
  }
278
278
  // ── Trading ──
279
279
  async marketOrder(symbol, side, size) {
280
+ const apiSymbol = this._toApi(symbol);
280
281
  const result = await this._signedPost("/fapi/v1/order", {
281
- symbol: this._toApi(symbol),
282
+ symbol: apiSymbol,
282
283
  side: side.toUpperCase(),
283
284
  type: "MARKET",
284
285
  quantity: size,
285
286
  });
286
- // Validate fill — Binance-style API returns 200 even for 0-fill orders
287
287
  const r = result;
288
288
  const executedQty = Number(r.executedQty ?? 0);
289
+ const orderId = String(r.orderId ?? "");
290
+ // If immediately filled, return
291
+ if (executedQty > 0 && r.status === "FILLED")
292
+ return result;
293
+ // status=NEW: order accepted but not yet filled — poll for up to 3 seconds
294
+ if (r.status === "NEW" && orderId) {
295
+ for (let i = 0; i < 3; i++) {
296
+ await new Promise(resolve => setTimeout(resolve, 1000));
297
+ try {
298
+ const check = await this._signedGet("/fapi/v1/order", {
299
+ symbol: apiSymbol,
300
+ orderId,
301
+ });
302
+ const filledQty = Number(check.executedQty ?? 0);
303
+ if (filledQty > 0)
304
+ return check; // filled
305
+ if (check.status === "CANCELED" || check.status === "EXPIRED" || check.status === "REJECTED") {
306
+ throw new Error(`Market ${side} ${symbol}: order ${check.status} (orderId: ${orderId})`);
307
+ }
308
+ }
309
+ catch (e) {
310
+ if (e instanceof Error && e.message.startsWith("Market "))
311
+ throw e;
312
+ // query failed, retry
313
+ }
314
+ }
315
+ // Still not filled after 3s — cancel and throw
316
+ try {
317
+ await this._signedDelete("/fapi/v1/order", { symbol: apiSymbol, orderId });
318
+ }
319
+ catch { /* best effort cancel */ }
320
+ throw new Error(`Market ${side} ${symbol}: order not filled after 3s, cancelled (orderId: ${orderId})`);
321
+ }
322
+ // executedQty === 0 with non-NEW status
289
323
  if (executedQty === 0) {
290
- throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${r.orderId})`);
324
+ throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${orderId})`);
291
325
  }
292
326
  return result;
293
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.30",
3
+ "version": "0.7.32",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",