perp-cli 0.7.30 → 0.7.36

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.
@@ -47,6 +47,8 @@ export declare class FundingArbStrategy implements Strategy {
47
47
  * HL getPositions() returns symbols like "PURR" or "PURR-PERP" — we normalize here.
48
48
  */
49
49
  private getPerpSymbol;
50
+ /** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP */
51
+ private findRate;
50
52
  /**
51
53
  * Get a price estimate for a symbol from the perp adapter.
52
54
  * Tries the base symbol first, then with -PERP suffix.
@@ -1,7 +1,7 @@
1
1
  import { registerStrategy } from "../strategy-registry.js";
2
2
  import { checkArbLiquidity, checkSpotPerpLiquidity } from "../../liquidity.js";
3
3
  import { computeMatchedSize, computeSpotPerpMatchedSize, reconcileArbFills } from "../../arb-sizing.js";
4
- import { computeSignedAnnualSpread, annualizeRate } from "../../funding.js";
4
+ import { getFundingHours } from "../../funding.js";
5
5
  import { logExecution, pruneExecutionLog } from "../../execution-log.js";
6
6
  export class FundingArbStrategy {
7
7
  name = "funding-arb";
@@ -153,7 +153,9 @@ export class FundingArbStrategy {
153
153
  for (const [name, a] of adapters) {
154
154
  try {
155
155
  const payments = await a.getFundingPayments(200);
156
- const sum = payments.reduce((s, p) => s + parseFloat(p.payment), 0);
156
+ const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
157
+ const recent = payments.filter(p => p.time > oneDayAgo);
158
+ const sum = recent.reduce((s, p) => s + parseFloat(p.payment), 0);
157
159
  if (Math.abs(sum) > 0.001) {
158
160
  historicalFunding += sum;
159
161
  ctx.log(` [FUND] ${name}: $${sum.toFixed(4)} historical funding`);
@@ -193,7 +195,7 @@ export class FundingArbStrategy {
193
195
  const rates = await this.fetchRates(a, name);
194
196
  const map = new Map();
195
197
  for (const r of rates)
196
- map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals, maxLeverage: r.maxLeverage });
198
+ map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals, maxLeverage: r.maxLeverage, fundingHours: r.fundingHours });
197
199
  ratesByExchange.set(name, map);
198
200
  }
199
201
  // ── Fetch funding income periodically (every 5 minutes) ──
@@ -227,15 +229,19 @@ export class FundingArbStrategy {
227
229
  // For spot-perp: long side is spot (rate=0), short side is perp
228
230
  const perpExchange = p.shortExchange;
229
231
  const perpSymbol = this.getPerpSymbol(p.symbol, perpExchange);
230
- const shortRate = ratesByExchange.get(perpExchange)?.get(perpSymbol)?.rate
231
- ?? ratesByExchange.get(perpExchange)?.get(p.symbol)?.rate;
232
- signedSpread = shortRate !== undefined ? annualizeRate(shortRate, perpExchange) : 0;
232
+ const rateInfo = this.findRate(ratesByExchange, perpExchange, perpSymbol)
233
+ ?? this.findRate(ratesByExchange, perpExchange, p.symbol);
234
+ const shortRate = rateInfo?.rate;
235
+ const fundingH = rateInfo?.fundingHours ?? getFundingHours(perpExchange);
236
+ signedSpread = shortRate !== undefined ? (shortRate / fundingH) * 8760 * 100 : 0;
233
237
  }
234
238
  else {
235
- const longRate = ratesByExchange.get(p.longExchange)?.get(p.symbol)?.rate;
236
- const shortRate = ratesByExchange.get(p.shortExchange)?.get(p.symbol)?.rate;
237
- signedSpread = (longRate !== undefined && shortRate !== undefined)
238
- ? computeSignedAnnualSpread(shortRate, p.shortExchange, longRate, p.longExchange)
239
+ const longInfo = this.findRate(ratesByExchange, p.longExchange, p.symbol);
240
+ const shortInfo = this.findRate(ratesByExchange, p.shortExchange, p.symbol);
241
+ const longHourly = longInfo ? longInfo.rate / (longInfo.fundingHours ?? getFundingHours(p.longExchange)) : undefined;
242
+ const shortHourly = shortInfo ? shortInfo.rate / (shortInfo.fundingHours ?? getFundingHours(p.shortExchange)) : undefined;
243
+ signedSpread = (longHourly !== undefined && shortHourly !== undefined)
244
+ ? (shortHourly - longHourly) * 8760 * 100
239
245
  : 0;
240
246
  }
241
247
  const sign = signedSpread >= 0 ? "+" : "";
@@ -250,18 +256,21 @@ export class FundingArbStrategy {
250
256
  if (pos.mode === "spot-perp") {
251
257
  const perpExchange = pos.shortExchange;
252
258
  const perpSymbol = this.getPerpSymbol(pos.symbol, perpExchange);
253
- const shortRate = ratesByExchange.get(perpExchange)?.get(perpSymbol)?.rate
254
- ?? ratesByExchange.get(perpExchange)?.get(pos.symbol)?.rate;
255
- if (shortRate === undefined)
259
+ const rateInfo = this.findRate(ratesByExchange, perpExchange, perpSymbol)
260
+ ?? this.findRate(ratesByExchange, perpExchange, pos.symbol);
261
+ if (rateInfo === undefined)
256
262
  continue;
257
- signedSpread = annualizeRate(shortRate, perpExchange);
263
+ const fundingH = rateInfo.fundingHours ?? getFundingHours(perpExchange);
264
+ signedSpread = (rateInfo.rate / fundingH) * 8760 * 100;
258
265
  }
259
266
  else {
260
- const longRate = ratesByExchange.get(pos.longExchange)?.get(pos.symbol)?.rate;
261
- const shortRate = ratesByExchange.get(pos.shortExchange)?.get(pos.symbol)?.rate;
262
- if (longRate === undefined || shortRate === undefined)
267
+ const longInfo = this.findRate(ratesByExchange, pos.longExchange, pos.symbol);
268
+ const shortInfo = this.findRate(ratesByExchange, pos.shortExchange, pos.symbol);
269
+ if (longInfo === undefined || shortInfo === undefined)
263
270
  continue;
264
- signedSpread = computeSignedAnnualSpread(shortRate, pos.shortExchange, longRate, pos.longExchange);
271
+ const longHourly = longInfo.rate / (longInfo.fundingHours ?? getFundingHours(pos.longExchange));
272
+ const shortHourly = shortInfo.rate / (shortInfo.fundingHours ?? getFundingHours(pos.shortExchange));
273
+ signedSpread = (shortHourly - longHourly) * 8760 * 100;
265
274
  }
266
275
  // Close if funding flipped (losing money) or spread below close threshold
267
276
  const shouldClose = signedSpread < 0 || signedSpread < params.close_spread;
@@ -293,19 +302,32 @@ export class FundingArbStrategy {
293
302
  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
303
  this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
295
304
  }
305
+ if (!closeLongOk)
306
+ continue; // don't close short if long failed — keep hedge intact
296
307
  try {
297
308
  const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size);
298
309
  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
310
  }
300
311
  catch (err) {
301
312
  const msg = err instanceof Error ? err.message : String(err);
302
- ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg}`);
313
+ ctx.log(` [ARB] Close short leg failed for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
303
314
  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" } });
315
+ // Rollback: re-open long to restore hedge
316
+ try {
317
+ await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
318
+ ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
319
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
320
+ }
321
+ catch (rbErr) {
322
+ const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
323
+ ctx.log(` [ARB] CRITICAL: Close rollback failed for ${pos.symbol}: ${rbMsg}`);
324
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
325
+ }
326
+ this._failCooldown.set(`${pos.symbol}:close`, Date.now() + 5 * 60 * 1000);
327
+ continue;
304
328
  }
305
- if (closeLongOk) {
306
- toClose.push(pos);
307
- ctx.log(` [ARB] Closed ${pos.symbol} position`);
308
- }
329
+ toClose.push(pos);
330
+ ctx.log(` [ARB] Closed ${pos.symbol} position`);
309
331
  }
310
332
  }
311
333
  }
@@ -327,23 +349,27 @@ export class FundingArbStrategy {
327
349
  }
328
350
  // Perp-perp opportunities
329
351
  for (const sym of allSymbols) {
330
- let minRate = Infinity, maxRate = -Infinity;
352
+ let minHourly = Infinity, maxHourly = -Infinity;
331
353
  let minExchange = "", maxExchange = "";
354
+ let minRate = 0, maxRate = 0;
332
355
  for (const exName of exchangeNames) {
333
- const rate = ratesByExchange.get(exName)?.get(sym)?.rate;
334
- if (rate === undefined)
356
+ const rateInfo = this.findRate(ratesByExchange, exName, sym);
357
+ if (!rateInfo)
335
358
  continue;
336
- if (rate < minRate) {
337
- minRate = rate;
359
+ const hourlyRate = rateInfo.rate / (rateInfo.fundingHours ?? getFundingHours(exName));
360
+ if (hourlyRate < minHourly) {
361
+ minHourly = hourlyRate;
338
362
  minExchange = exName;
363
+ minRate = rateInfo.rate;
339
364
  }
340
- if (rate > maxRate) {
341
- maxRate = rate;
365
+ if (hourlyRate > maxHourly) {
366
+ maxHourly = hourlyRate;
342
367
  maxExchange = exName;
368
+ maxRate = rateInfo.rate;
343
369
  }
344
370
  }
345
371
  if (minExchange && maxExchange && minExchange !== maxExchange) {
346
- const spread = computeSignedAnnualSpread(maxRate, maxExchange, minRate, minExchange);
372
+ const spread = (maxHourly - minHourly) * 8760 * 100;
347
373
  if (spread >= params.min_spread) {
348
374
  opportunities.push({
349
375
  symbol: sym,
@@ -369,14 +395,16 @@ export class FundingArbStrategy {
369
395
  const perpRates = ratesByExchange.get(name);
370
396
  if (!perpRates)
371
397
  continue;
372
- for (const [sym, { rate }] of perpRates) {
398
+ for (const [sym, rateEntry] of perpRates) {
399
+ const { rate } = rateEntry;
373
400
  // Only consider positive funding (shorts receive, longs pay)
374
401
  if (rate <= 0)
375
402
  continue;
376
403
  const base = sym.replace(/-PERP$/, "").toUpperCase();
377
404
  if (!spotSymbols.has(base))
378
405
  continue;
379
- const annualSpread = annualizeRate(rate, name);
406
+ const hourlyRate = rate / (rateEntry.fundingHours ?? getFundingHours(name));
407
+ const annualSpread = hourlyRate * 8760 * 100;
380
408
  if (annualSpread >= spotPerpMinSpread) {
381
409
  opportunities.push({
382
410
  symbol: base,
@@ -432,13 +460,13 @@ export class FundingArbStrategy {
432
460
  const shortAdapter = adapters.get(best.shortExchange);
433
461
  if (!longAdapter || !shortAdapter)
434
462
  continue;
435
- const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
463
+ const price = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.price ?? 0;
436
464
  if (price <= 0)
437
465
  continue;
438
466
  // Determine leverage for this symbol
439
467
  const configLeverage = params.leverage ?? 3;
440
- const longMaxLev = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.maxLeverage ?? 1;
441
- const shortMaxLev = ratesByExchange.get(best.shortExchange)?.get(best.symbol)?.maxLeverage ?? 1;
468
+ const longMaxLev = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.maxLeverage ?? 1;
469
+ const shortMaxLev = this.findRate(ratesByExchange, best.shortExchange, best.symbol)?.maxLeverage ?? 1;
442
470
  const leverage = Math.min(configLeverage, longMaxLev, shortMaxLev);
443
471
  // Set leverage on both exchanges before ordering
444
472
  try {
@@ -455,12 +483,12 @@ export class FundingArbStrategy {
455
483
  longAdapter.getBalance(),
456
484
  shortAdapter.getBalance(),
457
485
  ]);
458
- // Check margin usage (skip if > 80%)
486
+ // Check margin usage (skip if > 70%)
459
487
  const longEquity = parseFloat(longBal.equity);
460
488
  const shortEquity = parseFloat(shortBal.equity);
461
489
  const longMarginPct = longEquity > 0 ? parseFloat(longBal.marginUsed) / longEquity * 100 : 0;
462
490
  const shortMarginPct = shortEquity > 0 ? parseFloat(shortBal.marginUsed) / shortEquity * 100 : 0;
463
- if (longMarginPct > 80 || shortMarginPct > 80) {
491
+ if (longMarginPct > 70 || shortMarginPct > 70) {
464
492
  skipMargin++;
465
493
  continue;
466
494
  }
@@ -486,8 +514,8 @@ export class FundingArbStrategy {
486
514
  if (!liq.viable)
487
515
  continue;
488
516
  // Compute matched size (use actual precision from exchange market data)
489
- const longDec = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.sizeDecimals;
490
- const shortDec = ratesByExchange.get(best.shortExchange)?.get(best.symbol)?.sizeDecimals;
517
+ const longDec = this.findRate(ratesByExchange, best.longExchange, best.symbol)?.sizeDecimals;
518
+ const shortDec = this.findRate(ratesByExchange, best.shortExchange, best.symbol)?.sizeDecimals;
491
519
  let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { longSizeDecimals: longDec, shortSizeDecimals: shortDec });
492
520
  if (!matched) {
493
521
  ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
@@ -541,6 +569,39 @@ export class FundingArbStrategy {
541
569
  const reconMsg = reconErr instanceof Error ? reconErr.message : String(reconErr);
542
570
  ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
543
571
  }
572
+ // Verify both positions actually exist (catches instant liquidation)
573
+ try {
574
+ const [longPositions, shortPositions] = await Promise.all([
575
+ longAdapter.getPositions(),
576
+ shortAdapter.getPositions(),
577
+ ]);
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) {
581
+ ctx.log(` [ARB] Position verification failed for ${best.symbol}: long=${longExists}, short=${shortExists}`);
582
+ 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
+ if (longExists && !shortExists) {
585
+ try {
586
+ await longAdapter.marketOrder(best.symbol, "sell", matched.size);
587
+ ctx.log(` [ARB] Rollback: closed phantom long ${best.symbol}`);
588
+ }
589
+ catch { /* best effort */ }
590
+ }
591
+ if (shortExists && !longExists) {
592
+ try {
593
+ await shortAdapter.marketOrder(best.symbol, "buy", matched.size);
594
+ ctx.log(` [ARB] Rollback: closed phantom short ${best.symbol}`);
595
+ }
596
+ catch { /* best effort */ }
597
+ }
598
+ this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 10 * 60 * 1000);
599
+ continue;
600
+ }
601
+ }
602
+ catch (verifyErr) {
603
+ ctx.log(` [ARB] Position verification error for ${best.symbol}: ${verifyErr instanceof Error ? verifyErr.message : String(verifyErr)}`);
604
+ }
544
605
  // Track position
545
606
  const currentPositions = ctx.state.get("arbOpenPositions");
546
607
  currentPositions.push({
@@ -698,8 +759,8 @@ export class FundingArbStrategy {
698
759
  // Determine leverage for perp leg
699
760
  const configLeverage = params.leverage ?? 3;
700
761
  const perpSymbolUpper = this.getPerpSymbol(opp.symbol, opp.shortExchange).toUpperCase();
701
- const perpMaxLev = ratesByExchange.get(opp.shortExchange)?.get(perpSymbolUpper)?.maxLeverage
702
- ?? ratesByExchange.get(opp.shortExchange)?.get(opp.symbol.toUpperCase())?.maxLeverage
762
+ const perpMaxLev = this.findRate(ratesByExchange, opp.shortExchange, perpSymbolUpper)?.maxLeverage
763
+ ?? this.findRate(ratesByExchange, opp.shortExchange, opp.symbol.toUpperCase())?.maxLeverage
703
764
  ?? 1;
704
765
  const leverage = Math.min(configLeverage, perpMaxLev);
705
766
  // Set leverage on perp before ordering
@@ -714,7 +775,7 @@ export class FundingArbStrategy {
714
775
  // Check margin usage
715
776
  const perpEquity = parseFloat(perpBal.equity);
716
777
  const perpMarginPct = perpEquity > 0 ? parseFloat(perpBal.marginUsed) / perpEquity * 100 : 0;
717
- if (perpMarginPct > 80) {
778
+ if (perpMarginPct > 70) {
718
779
  ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: perp margin usage too high (${perpMarginPct.toFixed(0)}%)`);
719
780
  return false;
720
781
  }
@@ -888,14 +949,20 @@ export class FundingArbStrategy {
888
949
  * HL getPositions() returns symbols like "PURR" or "PURR-PERP" — we normalize here.
889
950
  */
890
951
  getPerpSymbol(baseSymbol, exchangeName) {
891
- // Strip any existing -PERP suffix first
892
952
  const base = baseSymbol.replace(/-PERP$/, "").toUpperCase();
893
- // HL perp symbols are just the base (e.g. "PURR", "ETH", "BTC")
894
- // Some HIP-3 tokens might need "-PERP" suffix — use base for now
895
- // since getPositions() returns the base form on HL
896
953
  void exchangeName;
897
954
  return base;
898
955
  }
956
+ /** Look up rate from ratesByExchange, trying symbol, symbol-PERP, and symbol without -PERP */
957
+ findRate(ratesByExchange, exchange, symbol) {
958
+ const map = ratesByExchange.get(exchange);
959
+ if (!map)
960
+ return undefined;
961
+ const upper = symbol.toUpperCase();
962
+ return map.get(upper)
963
+ ?? map.get(upper + "-PERP")
964
+ ?? map.get(upper.replace(/-PERP$/, ""));
965
+ }
899
966
  /**
900
967
  * Get a price estimate for a symbol from the perp adapter.
901
968
  * Tries the base symbol first, then with -PERP suffix.
@@ -923,6 +990,7 @@ export class FundingArbStrategy {
923
990
  price: parseFloat(m.markPrice),
924
991
  sizeDecimals: m.sizeDecimals,
925
992
  maxLeverage: m.maxLeverage,
993
+ fundingHours: m.fundingHours,
926
994
  }));
927
995
  }
928
996
  catch {
@@ -77,6 +77,12 @@ export class AsterAdapter {
77
77
  const maxLev = Number(s.maxLeverage ?? 50);
78
78
  const lotFilter = s.filters
79
79
  ?.find(f => f.filterType === "LOT_SIZE");
80
+ const nextFunding = Number(premium?.nextFundingTime ?? 0);
81
+ const hoursFromMidnight = nextFunding > 0 ? (nextFunding % 86400000) / 3600000 : 0;
82
+ const fundingHours = nextFunding <= 0 ? 1
83
+ : hoursFromMidnight % 8 === 0 ? 8
84
+ : hoursFromMidnight % 4 === 0 ? 4
85
+ : 1;
80
86
  return {
81
87
  symbol: this._fromApi(sym),
82
88
  markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
@@ -87,6 +93,7 @@ export class AsterAdapter {
87
93
  maxLeverage: maxLev,
88
94
  sizeDecimals: s.quantityPrecision != null ? Number(s.quantityPrecision) : undefined,
89
95
  stepSize: lotFilter?.stepSize != null ? String(lotFilter.stepSize) : undefined,
96
+ fundingHours,
90
97
  };
91
98
  });
92
99
  this._marketsCache = result;
@@ -277,17 +284,51 @@ export class AsterAdapter {
277
284
  }
278
285
  // ── Trading ──
279
286
  async marketOrder(symbol, side, size) {
287
+ const apiSymbol = this._toApi(symbol);
280
288
  const result = await this._signedPost("/fapi/v1/order", {
281
- symbol: this._toApi(symbol),
289
+ symbol: apiSymbol,
282
290
  side: side.toUpperCase(),
283
291
  type: "MARKET",
284
292
  quantity: size,
285
293
  });
286
- // Validate fill — Binance-style API returns 200 even for 0-fill orders
287
294
  const r = result;
288
295
  const executedQty = Number(r.executedQty ?? 0);
296
+ const orderId = String(r.orderId ?? "");
297
+ // If immediately filled, return
298
+ if (executedQty > 0 && r.status === "FILLED")
299
+ return result;
300
+ // status=NEW: order accepted but not yet filled — poll for up to 3 seconds
301
+ if (r.status === "NEW" && orderId) {
302
+ for (let i = 0; i < 3; i++) {
303
+ await new Promise(resolve => setTimeout(resolve, 1000));
304
+ try {
305
+ const check = await this._signedGet("/fapi/v1/order", {
306
+ symbol: apiSymbol,
307
+ orderId,
308
+ });
309
+ const filledQty = Number(check.executedQty ?? 0);
310
+ if (filledQty > 0)
311
+ return check; // filled
312
+ if (check.status === "CANCELED" || check.status === "EXPIRED" || check.status === "REJECTED") {
313
+ throw new Error(`Market ${side} ${symbol}: order ${check.status} (orderId: ${orderId})`);
314
+ }
315
+ }
316
+ catch (e) {
317
+ if (e instanceof Error && e.message.startsWith("Market "))
318
+ throw e;
319
+ // query failed, retry
320
+ }
321
+ }
322
+ // Still not filled after 3s — cancel and throw
323
+ try {
324
+ await this._signedDelete("/fapi/v1/order", { symbol: apiSymbol, orderId });
325
+ }
326
+ catch { /* best effort cancel */ }
327
+ throw new Error(`Market ${side} ${symbol}: order not filled after 3s, cancelled (orderId: ${orderId})`);
328
+ }
329
+ // executedQty === 0 with non-NEW status
289
330
  if (executedQty === 0) {
290
- throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${r.orderId})`);
331
+ throw new Error(`Market ${side} ${symbol}: order accepted but 0 filled (status: ${r.status}, orderId: ${orderId})`);
291
332
  }
292
333
  return result;
293
334
  }
@@ -12,6 +12,7 @@ export interface ExchangeMarketInfo {
12
12
  maxLeverage: number;
13
13
  sizeDecimals?: number;
14
14
  stepSize?: string;
15
+ fundingHours?: number;
15
16
  }
16
17
  export interface ExchangePosition {
17
18
  symbol: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.30",
3
+ "version": "0.7.36",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",