perp-cli 0.7.36 → 0.7.39

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.
@@ -42,6 +42,7 @@ export interface FundingArbStrategyParams {
42
42
  max_positions: number;
43
43
  exchanges: string[];
44
44
  leverage?: number;
45
+ min_hold_hours?: number;
45
46
  }
46
47
  export interface GenericStrategyParams {
47
48
  type: string;
@@ -16,6 +16,7 @@ export class FundingArbStrategy {
16
16
  { name: "max_positions", type: "number", required: false, default: 3, description: "Max concurrent positions" },
17
17
  { name: "exchanges", type: "string", required: true, description: "Comma-separated exchange names" },
18
18
  { name: "leverage", type: "number", required: false, default: 3, description: "Max leverage to use" },
19
+ { name: "min_hold_hours", type: "number", required: false, default: 2, description: "Min hold time before close (hours)" },
19
20
  ],
20
21
  };
21
22
  }
@@ -88,7 +89,7 @@ export class FundingArbStrategy {
88
89
  const longEx = pA.side === "long" ? exA : exB;
89
90
  const shortEx = pA.side === "short" ? exA : exB;
90
91
  const size = pA.side === "long" ? pA.size : pB.size;
91
- recovered.push({ symbol: pA.symbol, longExchange: longEx, shortExchange: shortEx, entrySpread: 0, size, mode: "perp-perp" });
92
+ recovered.push({ symbol: pA.symbol, longExchange: longEx, shortExchange: shortEx, entrySpread: 0, size, mode: "perp-perp", entryTime: 0 });
92
93
  used.add(keyA);
93
94
  used.add(keyB);
94
95
  break;
@@ -135,7 +136,7 @@ export class FundingArbStrategy {
135
136
  const spotBal = nonUsdc.find(b => b.token.toUpperCase().replace(/-SPOT$/, "") === base);
136
137
  ctx.log(` [ARB] ${name} perp ${perp.symbol}(${perp.side}) → base=${base}, spotMatch=${spotBal ? spotBal.token : "none"}`);
137
138
  if (spotBal && perp.side === "short") {
138
- recovered.push({ symbol: base, longExchange: `${name}-spot`, shortExchange: name, entrySpread: 0, size: perp.size, mode: "spot-perp" });
139
+ recovered.push({ symbol: base, longExchange: `${name}-spot`, shortExchange: name, entrySpread: 0, size: perp.size, mode: "spot-perp", entryTime: 0 });
139
140
  used.add(perpKey);
140
141
  ctx.log(` [ARB] Detected spot-perp hedge: ${base} ${name}-spot↔${name}`);
141
142
  }
@@ -273,7 +274,10 @@ export class FundingArbStrategy {
273
274
  signedSpread = (shortHourly - longHourly) * 8760 * 100;
274
275
  }
275
276
  // Close if funding flipped (losing money) or spread below close threshold
276
- const shouldClose = signedSpread < 0 || signedSpread < params.close_spread;
277
+ const minHoldMs = (params.min_hold_hours ?? 2) * 60 * 60 * 1000;
278
+ const holdTime = pos.entryTime > 0 ? Date.now() - pos.entryTime : Infinity;
279
+ const isFundingFlipped = signedSpread < 0;
280
+ const shouldClose = isFundingFlipped || (holdTime >= minHoldMs && signedSpread < params.close_spread);
277
281
  if (shouldClose) {
278
282
  // Cooldown: skip if close recently failed for this symbol
279
283
  const closeKey = `${pos.symbol}:close`;
@@ -570,17 +574,25 @@ export class FundingArbStrategy {
570
574
  ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
571
575
  }
572
576
  // Verify both positions actually exist (catches instant liquidation)
577
+ // Wait for cache to expire before checking (exchanges cache positions for ~5s)
578
+ await new Promise(resolve => setTimeout(resolve, 6000));
573
579
  try {
574
580
  const [longPositions, shortPositions] = await Promise.all([
575
581
  longAdapter.getPositions(),
576
582
  shortAdapter.getPositions(),
577
583
  ]);
578
- const longExists = longPositions.some(p => p.symbol.toUpperCase() === best.symbol.toUpperCase() && parseFloat(p.size) > 0);
579
- const shortExists = shortPositions.some(p => p.symbol.toUpperCase() === best.symbol.toUpperCase() && parseFloat(p.size) > 0);
580
- if (!longExists || !shortExists) {
584
+ const symUpper = best.symbol.toUpperCase();
585
+ const matchSymbol = (s) => { const u = s.toUpperCase(); return u === symUpper || u === symUpper + "-PERP" || u.replace(/-PERP$/, "") === symUpper; };
586
+ const longExists = longPositions.some(p => matchSymbol(p.symbol) && parseFloat(p.size) > 0);
587
+ const shortExists = shortPositions.some(p => matchSymbol(p.symbol) && parseFloat(p.size) > 0);
588
+ if (!longExists && !shortExists) {
589
+ // Both missing likely means cache issue — log warning but don't rollback
590
+ ctx.log(` [ARB] Position verification: both sides not visible for ${best.symbol} (may be cache delay)`);
591
+ }
592
+ else if (!longExists || !shortExists) {
593
+ // One side missing = instant liquidation — rollback the other
581
594
  ctx.log(` [ARB] Position verification failed for ${best.symbol}: long=${longExists}, short=${shortExists}`);
582
595
  logExecution({ type: "arb_entry", exchange: `${best.longExchange}↔${best.shortExchange}`, symbol: best.symbol, side: "verify", size: matched.size, status: "failed", error: `Position missing after fill: long=${longExists} short=${shortExists}`, dryRun: false });
583
- // Rollback whichever side exists
584
596
  if (longExists && !shortExists) {
585
597
  try {
586
598
  await longAdapter.marketOrder(best.symbol, "sell", matched.size);
@@ -611,6 +623,7 @@ export class FundingArbStrategy {
611
623
  entrySpread: best.spread,
612
624
  size: matched.size,
613
625
  mode: "perp-perp",
626
+ entryTime: Date.now(),
614
627
  });
615
628
  ctx.state.set("arbOpenPositions", currentPositions);
616
629
  ctx.state.set("arbPositions", currentPositions.length);
@@ -869,6 +882,7 @@ export class FundingArbStrategy {
869
882
  entrySpread: opp.spread,
870
883
  size: matched.size,
871
884
  mode: "spot-perp",
885
+ entryTime: Date.now(),
872
886
  });
873
887
  ctx.state.set("arbOpenPositions", currentPositions);
874
888
  ctx.state.set("arbPositions", currentPositions.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.36",
3
+ "version": "0.7.39",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",