perp-cli 0.7.37 → 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`;
@@ -577,8 +581,10 @@ export class FundingArbStrategy {
577
581
  longAdapter.getPositions(),
578
582
  shortAdapter.getPositions(),
579
583
  ]);
580
- const longExists = longPositions.some(p => p.symbol.toUpperCase() === best.symbol.toUpperCase() && parseFloat(p.size) > 0);
581
- const shortExists = shortPositions.some(p => p.symbol.toUpperCase() === best.symbol.toUpperCase() && parseFloat(p.size) > 0);
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);
582
588
  if (!longExists && !shortExists) {
583
589
  // Both missing likely means cache issue — log warning but don't rollback
584
590
  ctx.log(` [ARB] Position verification: both sides not visible for ${best.symbol} (may be cache delay)`);
@@ -617,6 +623,7 @@ export class FundingArbStrategy {
617
623
  entrySpread: best.spread,
618
624
  size: matched.size,
619
625
  mode: "perp-perp",
626
+ entryTime: Date.now(),
620
627
  });
621
628
  ctx.state.set("arbOpenPositions", currentPositions);
622
629
  ctx.state.set("arbPositions", currentPositions.length);
@@ -875,6 +882,7 @@ export class FundingArbStrategy {
875
882
  entrySpread: opp.spread,
876
883
  size: matched.size,
877
884
  mode: "spot-perp",
885
+ entryTime: Date.now(),
878
886
  });
879
887
  ctx.state.set("arbOpenPositions", currentPositions);
880
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.37",
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",