perp-cli 0.7.10 → 0.7.12

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.
@@ -141,8 +141,30 @@ export class FundingArbStrategy {
141
141
  if (longAdapter && shortAdapter) {
142
142
  const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
143
143
  if (price > 0) {
144
+ // Balance check — reduce size to fit available balance
145
+ let targetSizeUsd = params.size_usd;
146
+ try {
147
+ const [longBal, shortBal] = await Promise.all([
148
+ longAdapter.getBalance(),
149
+ shortAdapter.getBalance(),
150
+ ]);
151
+ const longAvail = parseFloat(longBal.available);
152
+ const shortAvail = parseFloat(shortBal.available);
153
+ const minAvail = Math.min(longAvail, shortAvail);
154
+ // Use at most 80% of available balance per leg
155
+ const maxAffordable = minAvail * 0.8;
156
+ if (maxAffordable < 10) {
157
+ ctx.log(` [ARB] Skip ${best.symbol}: insufficient balance ($${minAvail.toFixed(2)} available, need $10+ per leg)`);
158
+ return [];
159
+ }
160
+ if (targetSizeUsd > maxAffordable) {
161
+ ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance $${minAvail.toFixed(2)})`);
162
+ targetSizeUsd = Math.floor(maxAffordable);
163
+ }
164
+ }
165
+ catch { /* non-critical, proceed with original size */ }
144
166
  // Liquidity check
145
- const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, params.size_usd, 0.5, (msg) => ctx.log(` ${msg}`));
167
+ const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
146
168
  if (!liq.viable)
147
169
  return [];
148
170
  // Compute matched size
@@ -69,7 +69,8 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
69
69
  const bidOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "buy");
70
70
  const askOrders = state.openOrders.filter((o) => o.side.toLowerCase() === "sell");
71
71
  const hasMultiExchange = (state.exchangeBalances?.length ?? 0) > 1;
72
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), _jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsxs(Text, { color: "gray", children: ["$", parseFloat(eb.available).toFixed(2)] })] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
72
+ const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
73
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", (state.exchangeBalances?.reduce((s, e) => s + parseFloat(e.equity), 0) ?? state.equity).toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), _jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsxs(Text, { color: "gray", children: ["$", parseFloat(eb.available).toFixed(2)] })] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
73
74
  const upnl = parseFloat(p.unrealizedPnl);
74
75
  return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsxs(Text, { color: pnlColor(upnl), children: ["$", upnl.toFixed(2)] })] }, i));
75
76
  })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Strategy State" }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [Object.entries(state.strategyState).slice(0, 12).map(([k, v]) => (_jsxs(Box, { marginRight: 2, children: [_jsxs(Text, { color: "gray", children: [k, ": "] }), _jsx(Text, { color: "white", children: String(v) })] }, k))), Object.keys(state.strategyState).length === 0 && (_jsx(Text, { color: "gray", children: " --" }))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: MAX_LOG_LINES + 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Log" }), logs.length === 0 ? (_jsx(Text, { color: "gray", children: " Waiting for events..." })) : (logs.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [entry.time, " "] }), _jsx(Text, { children: colorizeLog(entry.message) })] }, i))))] }), _jsx(Box, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: _jsxs(Text, { color: "gray", children: ["[", _jsx(Text, { color: "white", bold: true, children: "Q" }), "]uit [", _jsx(Text, { color: "white", bold: true, children: "P" }), "]", paused ? "resume" : "ause"] }) })] }));
@@ -12,6 +12,9 @@ export declare class AsterAdapter implements ExchangeAdapter {
12
12
  private _apiSecret;
13
13
  private _baseUrl;
14
14
  private _testnet;
15
+ private _marketsCache;
16
+ private _marketsCacheTime;
17
+ private static readonly CACHE_TTL;
15
18
  constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
16
19
  get isReadOnly(): boolean;
17
20
  /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
@@ -55,6 +58,8 @@ export declare class AsterAdapter implements ExchangeAdapter {
55
58
  routeType?: number;
56
59
  }): Promise<unknown>;
57
60
  private _sign;
61
+ /** Handle 429 rate limit with retry */
62
+ private _handleResponse;
58
63
  private _publicGet;
59
64
  private _signedGet;
60
65
  private _signedPost;
@@ -12,6 +12,9 @@ export class AsterAdapter {
12
12
  _apiSecret;
13
13
  _baseUrl;
14
14
  _testnet;
15
+ _marketsCache = null;
16
+ _marketsCacheTime = 0;
17
+ static CACHE_TTL = 10_000; // 10 seconds
15
18
  constructor(apiKey, apiSecret, testnet = false) {
16
19
  this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
17
20
  this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
@@ -40,6 +43,10 @@ export class AsterAdapter {
40
43
  }
41
44
  // ── Market Data ──
42
45
  async getMarkets() {
46
+ // Return cached result if fresh
47
+ if (this._marketsCache && Date.now() - this._marketsCacheTime < AsterAdapter.CACHE_TTL) {
48
+ return this._marketsCache;
49
+ }
43
50
  const [info, tickers] = await Promise.all([
44
51
  this._publicGet("/fapi/v1/exchangeInfo"),
45
52
  this._publicGet("/fapi/v1/ticker/24hr"),
@@ -57,28 +64,12 @@ export class AsterAdapter {
57
64
  catch { /* non-critical */ }
58
65
  const tradingSymbols = (info?.symbols ?? [])
59
66
  .filter((s) => String(s.contractType) === "PERPETUAL" && String(s.status) === "TRADING");
60
- // Fetch open interest per symbol in parallel (batched to avoid overwhelming the API).
61
- // The /fapi/v1/ticker/24hr response does NOT include openInterest — need a separate endpoint.
62
- const oiMap = new Map();
63
- const OI_BATCH = 20;
64
- for (let i = 0; i < tradingSymbols.length; i += OI_BATCH) {
65
- const batch = tradingSymbols.slice(i, i + OI_BATCH);
66
- const results = await Promise.allSettled(batch.map(async (s) => {
67
- const sym = String(s.symbol);
68
- const res = await this._publicGet("/fapi/v1/openInterest", { symbol: sym });
69
- return { sym, oi: String(res?.openInterest ?? "0") };
70
- }));
71
- for (const r of results) {
72
- if (r.status === "fulfilled")
73
- oiMap.set(r.value.sym, r.value.oi);
74
- }
75
- }
76
- return tradingSymbols
67
+ // OI is fetched lazily per symbol (see getMarketInfo) to avoid 300+ API calls
68
+ const result = tradingSymbols
77
69
  .map((s) => {
78
70
  const sym = String(s.symbol);
79
71
  const ticker = tickerMap.get(sym);
80
72
  const premium = premiumMap.get(sym);
81
- // Extract max leverage from leverage brackets if available
82
73
  const maxLev = Number(s.maxLeverage ?? 50);
83
74
  return {
84
75
  symbol: this._fromApi(sym),
@@ -86,10 +77,13 @@ export class AsterAdapter {
86
77
  indexPrice: String(premium?.indexPrice ?? "0"),
87
78
  fundingRate: String(premium?.lastFundingRate ?? "0"),
88
79
  volume24h: String(ticker?.quoteVolume ?? ticker?.volume ?? "0"),
89
- openInterest: oiMap.get(sym) ?? "0",
80
+ openInterest: "0",
90
81
  maxLeverage: maxLev,
91
82
  };
92
83
  });
84
+ this._marketsCache = result;
85
+ this._marketsCacheTime = Date.now();
86
+ return result;
93
87
  }
94
88
  async getOrderbook(symbol) {
95
89
  const res = await this._publicGet("/fapi/v1/depth", { symbol: this._toApi(symbol), limit: "50" });
@@ -359,62 +353,82 @@ export class AsterAdapter {
359
353
  .digest("hex");
360
354
  return `${queryString}&signature=${signature}`;
361
355
  }
356
+ /** Handle 429 rate limit with retry */
357
+ async _handleResponse(res, method, path, attempt = 0) {
358
+ if (res.status === 429) {
359
+ if (attempt >= 2)
360
+ throw new Error(`${method} ${path} rate limited after ${attempt + 1} attempts`);
361
+ const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
362
+ const waitMs = Math.min(retryAfter * 1000, 30000);
363
+ await new Promise(r => setTimeout(r, waitMs));
364
+ return null; // signal retry
365
+ }
366
+ if (!res.ok) {
367
+ const text = await res.text();
368
+ throw new Error(`${method} ${path} failed (${res.status}): ${text.slice(0, 200)}`);
369
+ }
370
+ return res.json();
371
+ }
362
372
  async _publicGet(path, params = {}) {
363
373
  const qs = Object.entries(params)
364
374
  .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
365
375
  .join("&");
366
376
  const url = `${this._baseUrl}${path}${qs ? `?${qs}` : ""}`;
367
- const res = await fetch(url);
368
- if (!res.ok) {
369
- const text = await res.text();
370
- throw new Error(`GET ${path} failed (${res.status}): ${text.slice(0, 200)}`);
377
+ for (let attempt = 0; attempt < 3; attempt++) {
378
+ const res = await fetch(url);
379
+ const result = await this._handleResponse(res, "GET", path, attempt);
380
+ if (result !== null)
381
+ return result;
371
382
  }
372
- return res.json();
383
+ throw new Error(`GET ${path} failed: max retries exceeded`);
373
384
  }
374
385
  async _signedGet(path, params = {}) {
375
386
  if (this.isReadOnly)
376
387
  throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
377
- const signed = this._sign({ ...params });
378
- const url = `${this._baseUrl}${path}?${signed}`;
379
- const res = await fetch(url, {
380
- headers: { "X-MBX-APIKEY": this._apiKey },
381
- });
382
- if (!res.ok) {
383
- const text = await res.text();
384
- throw new Error(`GET ${path} failed (${res.status}): ${text.slice(0, 200)}`);
388
+ for (let attempt = 0; attempt < 3; attempt++) {
389
+ const signed = this._sign({ ...params });
390
+ const url = `${this._baseUrl}${path}?${signed}`;
391
+ const res = await fetch(url, {
392
+ headers: { "X-MBX-APIKEY": this._apiKey },
393
+ });
394
+ const result = await this._handleResponse(res, "GET", path, attempt);
395
+ if (result !== null)
396
+ return result;
385
397
  }
386
- return res.json();
398
+ throw new Error(`GET ${path} failed: max retries exceeded`);
387
399
  }
388
400
  async _signedPost(path, params = {}) {
389
401
  if (this.isReadOnly)
390
402
  throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
391
- const signed = this._sign({ ...params });
392
- const res = await fetch(`${this._baseUrl}${path}`, {
393
- method: "POST",
394
- headers: {
395
- "X-MBX-APIKEY": this._apiKey,
396
- "Content-Type": "application/x-www-form-urlencoded",
397
- },
398
- body: signed,
399
- });
400
- if (!res.ok) {
401
- const text = await res.text();
402
- throw new Error(`POST ${path} failed (${res.status}): ${text.slice(0, 200)}`);
403
+ for (let attempt = 0; attempt < 3; attempt++) {
404
+ const signed = this._sign({ ...params });
405
+ const res = await fetch(`${this._baseUrl}${path}`, {
406
+ method: "POST",
407
+ headers: {
408
+ "X-MBX-APIKEY": this._apiKey,
409
+ "Content-Type": "application/x-www-form-urlencoded",
410
+ },
411
+ body: signed,
412
+ });
413
+ const result = await this._handleResponse(res, "POST", path, attempt);
414
+ if (result !== null)
415
+ return result;
403
416
  }
404
- return res.json();
417
+ throw new Error(`POST ${path} failed: max retries exceeded`);
405
418
  }
406
419
  async _signedDelete(path, params = {}) {
407
420
  if (this.isReadOnly)
408
421
  throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
409
- const signed = this._sign({ ...params });
410
- const res = await fetch(`${this._baseUrl}${path}?${signed}`, {
411
- method: "DELETE",
412
- headers: { "X-MBX-APIKEY": this._apiKey },
413
- });
414
- if (!res.ok) {
415
- const text = await res.text();
416
- throw new Error(`DELETE ${path} failed (${res.status}): ${text.slice(0, 200)}`);
422
+ for (let attempt = 0; attempt < 3; attempt++) {
423
+ const signed = this._sign({ ...params });
424
+ const res = await fetch(`${this._baseUrl}${path}?${signed}`, {
425
+ method: "DELETE",
426
+ headers: { "X-MBX-APIKEY": this._apiKey },
427
+ });
428
+ const result = await this._handleResponse(res, "DELETE", path, attempt);
429
+ if (result !== null)
430
+ return result;
417
431
  }
418
- return res.json();
432
+ throw new Error(`DELETE ${path} failed: max retries exceeded`);
419
433
  }
420
434
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.10",
3
+ "version": "0.7.12",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",