perp-cli 0.7.19 → 0.7.21

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.
@@ -44,7 +44,7 @@ export async function getMarketSnapshot(adapter, symbol) {
44
44
  throw new Error(`Market "${symbol}" not found on ${adapter.name}`);
45
45
  }
46
46
  const price = parseFloat(m.markPrice);
47
- const fundingRate = parseFloat(m.fundingRate);
47
+ const fundingRate = parseFloat(m.fundingRate ?? "0");
48
48
  const volume24h = parseFloat(m.volume24h);
49
49
  // Try to get kline data for 24h high/low + RSI
50
50
  let high24h = price;
@@ -2,6 +2,7 @@ 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
4
  import { computeSignedAnnualSpread, annualizeRate } from "../../funding.js";
5
+ import { logExecution, pruneExecutionLog } from "../../execution-log.js";
5
6
  export class FundingArbStrategy {
6
7
  name = "funding-arb";
7
8
  describe() {
@@ -22,6 +23,11 @@ export class FundingArbStrategy {
22
23
  }
23
24
  _config = {};
24
25
  async init(ctx, _snapshot) {
26
+ // Prune old execution logs (keep 30 days)
27
+ try {
28
+ pruneExecutionLog(30);
29
+ }
30
+ catch { /* non-critical */ }
25
31
  this._config = ctx.config;
26
32
  const params = this.params;
27
33
  ctx.state.set("arbRunning", true);
@@ -181,7 +187,7 @@ export class FundingArbStrategy {
181
187
  const rates = await this.fetchRates(a, name);
182
188
  const map = new Map();
183
189
  for (const r of rates)
184
- map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
190
+ map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price, sizeDecimals: r.sizeDecimals });
185
191
  ratesByExchange.set(name, map);
186
192
  }
187
193
  // ── Fetch funding income periodically (every 5 minutes) ──
@@ -271,6 +277,7 @@ export class FundingArbStrategy {
271
277
  ]);
272
278
  toClose.push(pos);
273
279
  ctx.log(` [ARB] Closed ${pos.symbol} position`);
280
+ logExecution({ type: "arb_close", exchange: `${pos.longExchange}↔${pos.shortExchange}`, symbol: pos.symbol, side: "close", size: pos.size, status: "success", dryRun: false, meta: { mode: pos.mode, signedSpread: signedSpread } });
274
281
  }
275
282
  catch (err) {
276
283
  const msg = err instanceof Error ? err.message : String(err);
@@ -373,110 +380,116 @@ export class FundingArbStrategy {
373
380
  ctx.log(` [ARB] ${opp.symbol}: ${opp.spread.toFixed(1)}% spread (long ${opp.longExchange} ${(opp.longRate * 100).toFixed(4)}% / short ${opp.shortExchange} ${(opp.shortRate * 100).toFixed(4)}%)`);
374
381
  }
375
382
  }
376
- // Auto-execute if under position limit
377
- if (arbPositions < params.max_positions && opportunities.length > 0) {
378
- const best = opportunities[0];
379
- if (best.mode === "spot-perp") {
380
- await this.openSpotPerp(best, adapters, ctx);
381
- }
382
- else {
383
+ // Auto-execute: try each opportunity until one succeeds
384
+ if (arbPositions < params.max_positions) {
385
+ for (const best of opportunities) {
386
+ if (best.mode === "spot-perp") {
387
+ const opened = await this.openSpotPerp(best, adapters, ctx);
388
+ if (opened)
389
+ return [{ type: "noop" }];
390
+ continue;
391
+ }
383
392
  const longAdapter = adapters.get(best.longExchange);
384
393
  const shortAdapter = adapters.get(best.shortExchange);
385
- if (longAdapter && shortAdapter) {
386
- const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
387
- if (price > 0) {
388
- // Balance check
389
- let targetSizeUsd = params.size_usd;
390
- try {
391
- const [longBal, shortBal] = await Promise.all([
392
- longAdapter.getBalance(),
393
- shortAdapter.getBalance(),
394
- ]);
395
- const longAvail = parseFloat(longBal.available);
396
- const shortAvail = parseFloat(shortBal.available);
397
- const minAvail = Math.min(longAvail, shortAvail);
398
- const maxAffordable = minAvail * 0.8;
399
- if (maxAffordable < 10) {
400
- ctx.log(` [ARB] Skip ${best.symbol}: insufficient balance ($${minAvail.toFixed(2)} available, need $10+ per leg)`);
401
- return [];
402
- }
403
- if (targetSizeUsd > maxAffordable) {
404
- ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance $${minAvail.toFixed(2)})`);
405
- targetSizeUsd = Math.floor(maxAffordable);
394
+ if (!longAdapter || !shortAdapter)
395
+ continue;
396
+ const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
397
+ if (price <= 0)
398
+ continue;
399
+ // Balance check
400
+ let targetSizeUsd = params.size_usd;
401
+ try {
402
+ const [longBal, shortBal] = await Promise.all([
403
+ longAdapter.getBalance(),
404
+ shortAdapter.getBalance(),
405
+ ]);
406
+ const longAvail = parseFloat(longBal.available);
407
+ const shortAvail = parseFloat(shortBal.available);
408
+ const minAvail = Math.min(longAvail, shortAvail);
409
+ const maxAffordable = minAvail * 0.8;
410
+ if (maxAffordable < 10) {
411
+ ctx.log(` [ARB] Skip ${best.symbol}: insufficient balance ($${minAvail.toFixed(2)} available)`);
412
+ continue;
413
+ }
414
+ if (targetSizeUsd > maxAffordable) {
415
+ ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance $${minAvail.toFixed(2)})`);
416
+ targetSizeUsd = Math.floor(maxAffordable);
417
+ }
418
+ }
419
+ catch { /* non-critical, proceed with original size */ }
420
+ // Liquidity check
421
+ const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
422
+ if (!liq.viable)
423
+ continue;
424
+ // Compute matched size (use actual precision from exchange market data)
425
+ const longDec = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.sizeDecimals;
426
+ const shortDec = ratesByExchange.get(best.shortExchange)?.get(best.symbol)?.sizeDecimals;
427
+ let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { longSizeDecimals: longDec, shortSizeDecimals: shortDec });
428
+ if (!matched) {
429
+ ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
430
+ continue;
431
+ }
432
+ try {
433
+ ctx.log(` [ARB] Opening: ${matched.size} ${best.symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)`);
434
+ try {
435
+ await Promise.all([
436
+ longAdapter.marketOrder(best.symbol, "buy", matched.size),
437
+ shortAdapter.marketOrder(best.symbol, "sell", matched.size),
438
+ ]);
439
+ }
440
+ catch (orderErr) {
441
+ const errMsg = orderErr instanceof Error ? orderErr.message : String(orderErr);
442
+ const lotMatch = errMsg.match(/not a multiple of lot size (\d+(?:\.\d+)?)/);
443
+ if (lotMatch) {
444
+ const lotSize = parseFloat(lotMatch[1]);
445
+ ctx.log(` [ARB] Lot size ${lotSize} detected, recalculating...`);
446
+ matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize, longSizeDecimals: longDec, shortSizeDecimals: shortDec });
447
+ if (!matched) {
448
+ ctx.log(` [ARB] Skip ${best.symbol}: can't meet lot size ${lotSize}`);
449
+ continue;
406
450
  }
451
+ ctx.log(` [ARB] Retry: ${matched.size} ${best.symbol} ($${matched.notional.toFixed(0)}/leg)`);
452
+ await Promise.all([
453
+ longAdapter.marketOrder(best.symbol, "buy", matched.size),
454
+ shortAdapter.marketOrder(best.symbol, "sell", matched.size),
455
+ ]);
407
456
  }
408
- catch { /* non-critical, proceed with original size */ }
409
- // Liquidity check
410
- const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
411
- if (!liq.viable)
412
- return [];
413
- // Compute matched size
414
- let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
415
- if (!matched) {
416
- ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
417
- return [];
418
- }
419
- try {
420
- ctx.log(` [ARB] Opening: ${matched.size} ${best.symbol} on both legs ($${matched.notional.toFixed(0)}/leg, slippage ~${liq.longSlippage.toFixed(2)}%/${liq.shortSlippage.toFixed(2)}%)`);
421
- try {
422
- await Promise.all([
423
- longAdapter.marketOrder(best.symbol, "buy", matched.size),
424
- shortAdapter.marketOrder(best.symbol, "sell", matched.size),
425
- ]);
426
- }
427
- catch (orderErr) {
428
- // Parse lot size from error and retry with corrected size
429
- const errMsg = orderErr instanceof Error ? orderErr.message : String(orderErr);
430
- const lotMatch = errMsg.match(/not a multiple of lot size (\d+(?:\.\d+)?)/);
431
- if (lotMatch) {
432
- const lotSize = parseFloat(lotMatch[1]);
433
- ctx.log(` [ARB] Lot size ${lotSize} detected, recalculating...`);
434
- matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize });
435
- if (!matched) {
436
- ctx.log(` [ARB] Skip ${best.symbol}: can't meet lot size ${lotSize}`);
437
- return [];
438
- }
439
- ctx.log(` [ARB] Retry: ${matched.size} ${best.symbol} ($${matched.notional.toFixed(0)}/leg)`);
440
- await Promise.all([
441
- longAdapter.marketOrder(best.symbol, "buy", matched.size),
442
- shortAdapter.marketOrder(best.symbol, "sell", matched.size),
443
- ]);
444
- }
445
- else {
446
- throw orderErr;
447
- }
448
- }
449
- // Verify fills match, correct if needed
450
- try {
451
- const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => ctx.log(` ${msg}`));
452
- if (!recon.matched) {
453
- ctx.log(` [ARB] WARNING: fills not matched after correction attempt`);
454
- }
455
- }
456
- catch (reconErr) {
457
- const reconMsg = reconErr instanceof Error ? reconErr.message : String(reconErr);
458
- ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
459
- }
460
- // Track position
461
- const currentPositions = ctx.state.get("arbOpenPositions");
462
- currentPositions.push({
463
- symbol: best.symbol,
464
- longExchange: best.longExchange,
465
- shortExchange: best.shortExchange,
466
- entrySpread: best.spread,
467
- size: matched.size,
468
- mode: "perp-perp",
469
- });
470
- ctx.state.set("arbOpenPositions", currentPositions);
471
- ctx.state.set("arbPositions", currentPositions.length);
472
- ctx.log(` [ARB] Position opened! (${currentPositions.length}/${params.max_positions})`);
473
- return [{ type: "noop" }];
457
+ else {
458
+ throw orderErr;
474
459
  }
475
- catch (err) {
476
- const msg = err instanceof Error ? err.message : String(err);
477
- ctx.log(` [ARB] Execution failed: ${msg}`);
460
+ }
461
+ // Verify fills match
462
+ try {
463
+ const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => ctx.log(` ${msg}`));
464
+ if (!recon.matched) {
465
+ ctx.log(` [ARB] WARNING: fills not matched after correction attempt`);
478
466
  }
479
467
  }
468
+ catch (reconErr) {
469
+ const reconMsg = reconErr instanceof Error ? reconErr.message : String(reconErr);
470
+ ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
471
+ }
472
+ // Track position
473
+ const currentPositions = ctx.state.get("arbOpenPositions");
474
+ currentPositions.push({
475
+ symbol: best.symbol,
476
+ longExchange: best.longExchange,
477
+ shortExchange: best.shortExchange,
478
+ entrySpread: best.spread,
479
+ size: matched.size,
480
+ mode: "perp-perp",
481
+ });
482
+ ctx.state.set("arbOpenPositions", currentPositions);
483
+ ctx.state.set("arbPositions", currentPositions.length);
484
+ ctx.log(` [ARB] Position opened! (${currentPositions.length}/${params.max_positions})`);
485
+ logExecution({ type: "arb_entry", exchange: `${best.longExchange}↔${best.shortExchange}`, symbol: best.symbol, side: "long/short", size: matched.size, notional: matched.notional, status: "success", dryRun: false, meta: { mode: "perp-perp", spread: best.spread } });
486
+ return [{ type: "noop" }];
487
+ }
488
+ catch (err) {
489
+ const msg = err instanceof Error ? err.message : String(err);
490
+ ctx.log(` [ARB] Execution failed for ${best.symbol}: ${msg}`);
491
+ logExecution({ type: "arb_entry", exchange: `${best.longExchange}↔${best.shortExchange}`, symbol: best.symbol, side: "long/short", size: matched?.size ?? "0", status: "failed", error: msg, dryRun: false, meta: { mode: "perp-perp" } });
492
+ continue;
480
493
  }
481
494
  }
482
495
  }
@@ -593,13 +606,13 @@ export class FundingArbStrategy {
593
606
  const perpAdapter = adapters.get(opp.shortExchange) ?? adapters.get(exchangeName);
594
607
  if (!perpAdapter) {
595
608
  ctx.log(` [ARB] Open spot-perp failed: no adapter for ${opp.shortExchange}`);
596
- return;
609
+ return false;
597
610
  }
598
611
  try {
599
612
  const spotAdapter = await this.getSpotAdapter(exchangeName, perpAdapter);
600
613
  if (!spotAdapter) {
601
614
  ctx.log(` [ARB] Open spot-perp failed: spot adapter not available for ${exchangeName}`);
602
- return;
615
+ return false;
603
616
  }
604
617
  // Balance check: need full notional for spot + margin for perp
605
618
  let targetSizeUsd = params.size_usd;
@@ -609,7 +622,7 @@ export class FundingArbStrategy {
609
622
  const maxAffordable = perpAvail * 0.8;
610
623
  if (maxAffordable < 10) {
611
624
  ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: insufficient balance ($${perpAvail.toFixed(2)} available)`);
612
- return;
625
+ return false;
613
626
  }
614
627
  if (targetSizeUsd > maxAffordable) {
615
628
  ctx.log(` [ARB] Spot-perp size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance)`);
@@ -621,12 +634,12 @@ export class FundingArbStrategy {
621
634
  const priceEstimate = await this.getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
622
635
  if (priceEstimate <= 0) {
623
636
  ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't determine price`);
624
- return;
637
+ return false;
625
638
  }
626
639
  // Liquidity check
627
640
  const liq = await checkSpotPerpLiquidity(spotAdapter, perpAdapter, opp.symbol, perpSymbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
628
641
  if (!liq.viable)
629
- return;
642
+ return false;
630
643
  // Compute matched size
631
644
  const spotMarkets = await spotAdapter.getSpotMarkets();
632
645
  const spotMarket = spotMarkets.find(m => m.baseToken.toUpperCase() === opp.symbol.toUpperCase());
@@ -634,7 +647,7 @@ export class FundingArbStrategy {
634
647
  const matched = computeSpotPerpMatchedSize(liq.adjustedSizeUsd, priceEstimate, exchangeName, opp.shortExchange, spotDecimals);
635
648
  if (!matched) {
636
649
  ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't compute matched size`);
637
- return;
650
+ return false;
638
651
  }
639
652
  ctx.log(` [ARB] Opening spot-perp: ${matched.size} ${opp.symbol} ($${matched.notional.toFixed(0)}, slippage ~${liq.spotSlippage.toFixed(2)}%/${liq.perpSlippage.toFixed(2)}%)`);
640
653
  // Step 1: Transfer USDC to spot
@@ -660,10 +673,14 @@ export class FundingArbStrategy {
660
673
  ctx.state.set("arbOpenPositions", currentPositions);
661
674
  ctx.state.set("arbPositions", currentPositions.length);
662
675
  ctx.log(` [ARB] Spot-perp position opened! (${currentPositions.length}/${params.max_positions})`);
676
+ logExecution({ type: "arb_entry", exchange: `${opp.longExchange}↔${opp.shortExchange}`, symbol: opp.symbol, side: "spot-long/perp-short", size: matched.size, notional: matched.notional, status: "success", dryRun: false, meta: { mode: "spot-perp", spread: opp.spread } });
677
+ return true;
663
678
  }
664
679
  catch (err) {
665
680
  const msg = err instanceof Error ? err.message : String(err);
666
681
  ctx.log(` [ARB] Open spot-perp failed for ${opp.symbol}: ${msg}`);
682
+ logExecution({ type: "arb_entry", exchange: opp.shortExchange, symbol: opp.symbol, side: "spot-long/perp-short", size: "0", status: "failed", error: msg, dryRun: false, meta: { mode: "spot-perp" } });
683
+ return false;
667
684
  }
668
685
  }
669
686
  /** Transfer USDC from perp to spot account (exchange-specific) */
@@ -760,10 +777,13 @@ export class FundingArbStrategy {
760
777
  async fetchRates(adapter, _exchangeName) {
761
778
  try {
762
779
  const markets = await adapter.getMarkets();
763
- return markets.map(m => ({
780
+ return markets
781
+ .filter(m => m.fundingRate != null)
782
+ .map(m => ({
764
783
  symbol: m.symbol,
765
784
  rate: parseFloat(m.fundingRate),
766
785
  price: parseFloat(m.markPrice),
786
+ sizeDecimals: m.sizeDecimals,
767
787
  }));
768
788
  }
769
789
  catch {
@@ -48,7 +48,7 @@ export class FundingArbV2Strategy {
48
48
  const map = new Map();
49
49
  for (const m of markets) {
50
50
  map.set(m.symbol.toUpperCase(), {
51
- rate: parseFloat(m.fundingRate),
51
+ rate: m.fundingRate != null ? parseFloat(m.fundingRate) : NaN,
52
52
  price: parseFloat(m.markPrice),
53
53
  });
54
54
  }
@@ -125,7 +125,7 @@ class FundingAutoStrategy {
125
125
  const sym = normalizeSymbol(raw);
126
126
  const price = parseFloat(m.markPrice);
127
127
  map.set(sym, {
128
- rate: parseFloat(m.fundingRate),
128
+ rate: m.fundingRate != null ? parseFloat(m.fundingRate) : NaN,
129
129
  price,
130
130
  exchange: name,
131
131
  });
@@ -15,7 +15,7 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
15
15
  chalk.white.bold(m.symbol),
16
16
  `$${formatUsd(m.markPrice)}`,
17
17
  `$${formatUsd(m.indexPrice)}`,
18
- formatPercent(m.fundingRate),
18
+ m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A"),
19
19
  `$${formatUsd(m.volume24h)}`,
20
20
  `$${formatUsd(m.openInterest)}`,
21
21
  String(m.maxLeverage) + "x",
@@ -98,7 +98,7 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
98
98
  chalk.white.bold(m.symbol),
99
99
  `$${formatUsd(m.markPrice)}`,
100
100
  `$${formatUsd(m.indexPrice)}`,
101
- formatPercent(m.fundingRate),
101
+ m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A"),
102
102
  ];
103
103
  if (!explicitExchange && getAdapterForExchange) {
104
104
  const grouped = {};
@@ -170,7 +170,7 @@ export function registerMarketCommands(program, getAdapter, isJson, getAdapterFo
170
170
  console.log(chalk.cyan.bold(`\n ${m.symbol} on ${adapter.name}\n`));
171
171
  console.log(` Mark Price: $${formatUsd(m.markPrice)}`);
172
172
  console.log(` Index Price: $${formatUsd(m.indexPrice)}`);
173
- console.log(` Funding Rate: ${formatPercent(m.fundingRate)}`);
173
+ console.log(` Funding Rate: ${m.fundingRate != null ? formatPercent(m.fundingRate) : chalk.gray("N/A")}`);
174
174
  console.log(` 24h Volume: $${formatUsd(m.volume24h)}`);
175
175
  console.log(` Open Interest: $${formatUsd(m.openInterest)}`);
176
176
  console.log(` Max Leverage: ${m.maxLeverage}x`);
@@ -75,14 +75,18 @@ export class AsterAdapter {
75
75
  const ticker = tickerMap.get(sym);
76
76
  const premium = premiumMap.get(sym);
77
77
  const maxLev = Number(s.maxLeverage ?? 50);
78
+ const lotFilter = s.filters
79
+ ?.find(f => f.filterType === "LOT_SIZE");
78
80
  return {
79
81
  symbol: this._fromApi(sym),
80
82
  markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
81
83
  indexPrice: String(premium?.indexPrice ?? "0"),
82
- fundingRate: String(premium?.lastFundingRate ?? "0"),
84
+ fundingRate: premium?.lastFundingRate != null ? String(premium.lastFundingRate) : null,
83
85
  volume24h: String(ticker?.quoteVolume ?? ticker?.volume ?? "0"),
84
86
  openInterest: "0",
85
87
  maxLeverage: maxLev,
88
+ sizeDecimals: s.quantityPrecision != null ? Number(s.quantityPrecision) : undefined,
89
+ stepSize: lotFilter?.stepSize != null ? String(lotFilter.stepSize) : undefined,
86
90
  };
87
91
  });
88
92
  this._marketsCache = result;
@@ -218,7 +218,7 @@ export class HyperliquidAdapter {
218
218
  symbol: sym,
219
219
  markPrice: String(ctx.markPx ?? mids[sym] ?? "0"),
220
220
  indexPrice: String(ctx.oraclePx ?? "0"),
221
- fundingRate: String(ctx.funding ?? "0"),
221
+ fundingRate: ctx.funding != null ? String(ctx.funding) : null,
222
222
  volume24h: String(ctx.dayNtlVlm ?? "0"),
223
223
  openInterest: String(ctx.openInterest ?? "0"),
224
224
  maxLeverage: Number(asset.maxLeverage ?? 50),
@@ -6,10 +6,12 @@ export interface ExchangeMarketInfo {
6
6
  symbol: string;
7
7
  markPrice: string;
8
8
  indexPrice: string;
9
- fundingRate: string;
9
+ fundingRate: string | null;
10
10
  volume24h: string;
11
11
  openInterest: string;
12
12
  maxLeverage: number;
13
+ sizeDecimals?: number;
14
+ stepSize?: string;
13
15
  }
14
16
  export interface ExchangePosition {
15
17
  symbol: string;
@@ -266,18 +266,22 @@ export class LighterAdapter {
266
266
  return this._marketsCache;
267
267
  const markets = [];
268
268
  try {
269
- const res = await this.restGet("/orderBookDetails", {});
270
- for (const d of res.order_book_details ?? []) {
269
+ // Fetch order book details and funding rates in parallel
270
+ const [obRes, fundingRates] = await Promise.all([
271
+ this.restGet("/orderBookDetails", {}),
272
+ this.getFundingRates().catch(() => new Map()),
273
+ ]);
274
+ for (const d of obRes.order_book_details ?? []) {
271
275
  if (d.market_type !== "perp")
272
276
  continue;
273
- // max_leverage from min_initial_margin_fraction (e.g. 400 = 4% margin = 25x)
274
277
  const imf = d.min_initial_margin_fraction ?? d.default_initial_margin_fraction ?? 500;
275
278
  const maxLev = imf > 0 ? Math.floor(10000 / imf) : 50;
279
+ const fr = fundingRates.get(d.symbol);
276
280
  markets.push({
277
281
  symbol: d.symbol,
278
- markPrice: String(d.last_trade_price ?? 0),
282
+ markPrice: fr?.markPrice ?? String(d.last_trade_price ?? 0),
279
283
  indexPrice: String(d.last_trade_price ?? 0),
280
- fundingRate: "0", // funding rates require auth
284
+ fundingRate: fr?.rate ?? null,
281
285
  volume24h: String(d.daily_quote_token_volume ?? 0),
282
286
  openInterest: String(d.open_interest ?? 0),
283
287
  maxLeverage: maxLev,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.19",
3
+ "version": "0.7.21",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",