perp-cli 0.9.2 → 0.9.3

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.
@@ -17,7 +17,7 @@ export declare function computeMatchedSize(sizeUsd: number, price: number, longE
17
17
  * Compute matched size for spot-perp arb.
18
18
  * Uses spot exchange's size decimals for the spot leg.
19
19
  */
20
- export declare function computeSpotPerpMatchedSize(sizeUsd: number, price: number, spotExchange: string, perpExchange: string, spotSizeDecimals?: number): {
20
+ export declare function computeSpotPerpMatchedSize(sizeUsd: number, price: number, spotExchange: string, perpExchange: string, spotSizeDecimals?: number, perpSizeDecimals?: number): {
21
21
  size: string;
22
22
  notional: number;
23
23
  } | null;
@@ -77,12 +77,12 @@ function getMinNotional(exchange) {
77
77
  * Compute matched size for spot-perp arb.
78
78
  * Uses spot exchange's size decimals for the spot leg.
79
79
  */
80
- export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExchange, spotSizeDecimals) {
80
+ export function computeSpotPerpMatchedSize(sizeUsd, price, spotExchange, perpExchange, spotSizeDecimals, perpSizeDecimals) {
81
81
  if (price <= 0)
82
82
  return null;
83
- // If explicit spot decimals provided, use them; otherwise derive from exchange
83
+ // If explicit decimals provided, use them; otherwise derive from exchange
84
84
  const spotDec = spotSizeDecimals ?? getSizeDecimals(`spot:${spotExchange}`);
85
- const perpDec = getSizeDecimals(perpExchange);
85
+ const perpDec = perpSizeDecimals ?? getSizeDecimals(perpExchange);
86
86
  const szDecimals = Math.min(spotDec, perpDec);
87
87
  const rawSize = sizeUsd / price;
88
88
  const factor = Math.pow(10, szDecimals);
@@ -114,6 +114,21 @@ mode = "headless") {
114
114
  const shutdown = () => { running = false; state.phase = "stopped"; };
115
115
  process.on("SIGINT", shutdown);
116
116
  process.on("SIGTERM", shutdown);
117
+ // Direct stdin 'q' handler as fallback (ink's useInput may not work in all terminals)
118
+ let stdinHandler;
119
+ if (process.stdin.isTTY) {
120
+ process.stdin.setRawMode?.(true);
121
+ process.stdin.resume();
122
+ stdinHandler = (data) => {
123
+ const ch = data.toString();
124
+ if (ch === "q" || ch === "Q" || ch === "\x03") { // q, Q, or Ctrl+C
125
+ shutdown();
126
+ if (tuiUnmount)
127
+ tuiUnmount();
128
+ }
129
+ };
130
+ process.stdin.on("data", stdinHandler);
131
+ }
117
132
  if (mode !== "tui") {
118
133
  // Header (headless / json modes print header to console)
119
134
  log(chalk.cyan.bold(`\n ╔═══════════════════════════════════════╗`));
@@ -308,6 +323,11 @@ mode = "headless") {
308
323
  finally {
309
324
  process.removeListener("SIGINT", shutdown);
310
325
  process.removeListener("SIGTERM", shutdown);
326
+ if (stdinHandler) {
327
+ process.stdin.removeListener("data", stdinHandler);
328
+ process.stdin.setRawMode?.(false);
329
+ process.stdin.pause();
330
+ }
311
331
  // Cleanup: cancel any remaining orders
312
332
  if (state.strategyActive) {
313
333
  effectiveLog(` Cleaning up orders...`);
@@ -68,9 +68,11 @@ export class SpotPerpExecutor {
68
68
  // Compute matched size
69
69
  const spotMarkets = await spotAdapter.getSpotMarkets();
70
70
  const spotDec = spotMarkets.find(m => m.baseToken.toUpperCase() === symbol.toUpperCase())?.sizeDecimals;
71
- const matched = computeSpotPerpMatchedSize(liq.adjustedSizeUsd, price, spotExchange, perpExchange, spotDec);
71
+ const perpMarkets = await perpAdapter.getMarkets();
72
+ const perpDec = perpMarkets.find(m => matchSymbol(m.symbol, perpSymbol) || matchSymbol(m.symbol, symbol))?.sizeDecimals;
73
+ const matched = computeSpotPerpMatchedSize(liq.adjustedSizeUsd, price, spotExchange, perpExchange, spotDec, perpDec);
72
74
  if (!matched) {
73
- log(` [SPA] Skip ${symbol}: matched size fail`);
75
+ log(` [SPA] Skip ${symbol}: matched size fail (spotDec=${spotDec}, perpDec=${perpDec}, price=${price.toFixed(2)}, sizeUsd=${liq.adjustedSizeUsd.toFixed(2)})`);
74
76
  return { success: false, error: "matched size fail" };
75
77
  }
76
78
  log(` [SPA] Opening: ${matched.size} ${symbol} ($${matched.notional.toFixed(0)}) ${spotExchange}-spot<>${perpExchange}`);
@@ -231,10 +231,10 @@ export class LighterSpotAdapter {
231
231
  orderType: 1, // MARKET
232
232
  timeInForce: 0, // IOC
233
233
  });
234
- // Verify fill via tx endpoint
234
+ // Verify fill via tx endpoint (best-effort: tx was accepted by sendTx, so likely executed)
235
235
  const txResult = result;
236
236
  if (txResult.tx_hash) {
237
- for (let attempt = 0; attempt < 5; attempt++) {
237
+ for (let attempt = 0; attempt < 10; attempt++) {
238
238
  await new Promise(r => setTimeout(r, 500));
239
239
  try {
240
240
  const tx = await this._restGet("/tx", { by: "hash", value: txResult.tx_hash });
@@ -249,7 +249,8 @@ export class LighterSpotAdapter {
249
249
  throw e;
250
250
  }
251
251
  }
252
- throw new Error(`Spot ${side} ${symbol}: tx not confirmed after 2.5s (hash=${txResult.tx_hash})`);
252
+ // tx was accepted (sendTx 200) but verification timed out — warn, don't throw
253
+ console.error(`[lighter-spot] Warning: spot ${side} ${symbol} tx not confirmed after 5s (hash=${txResult.tx_hash}) — assuming executed`);
253
254
  }
254
255
  return result;
255
256
  }
@@ -483,7 +483,7 @@ export class LighterAdapter {
483
483
  const txResult = result;
484
484
  if (txResult.tx_hash) {
485
485
  // Poll tx status (may take a moment to process)
486
- for (let attempt = 0; attempt < 5; attempt++) {
486
+ for (let attempt = 0; attempt < 10; attempt++) {
487
487
  await new Promise(r => setTimeout(r, 500));
488
488
  try {
489
489
  const tx = await this.restGet("/tx", { by: "hash", value: txResult.tx_hash });
@@ -519,8 +519,8 @@ export class LighterAdapter {
519
519
  // tx query failed, retry
520
520
  }
521
521
  }
522
- // After 5 attempts, tx still not processed
523
- throw new Error(`Market ${side} ${symbol}: tx not confirmed after 2.5s (hash=${txResult.tx_hash})`);
522
+ // tx was accepted (sendTx 200) but verification timed out — warn, don't throw
523
+ console.error(`[lighter] Warning: market ${side} ${symbol} tx not confirmed after 5s (hash=${txResult.tx_hash}) — assuming executed`);
524
524
  }
525
525
  return result;
526
526
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",