perp-cli 0.7.19 → 0.7.20
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.
- package/dist/bot/conditions.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +110 -104
- package/dist/bot/strategies/funding-arb-v2.js +1 -1
- package/dist/bot/strategies/funding-auto.js +1 -1
- package/dist/commands/market.js +3 -3
- package/dist/exchanges/aster.js +1 -1
- package/dist/exchanges/hyperliquid.js +1 -1
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter.js +9 -5
- package/package.json +1 -1
package/dist/bot/conditions.js
CHANGED
|
@@ -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;
|
|
@@ -373,110 +373,112 @@ export class FundingArbStrategy {
|
|
|
373
373
|
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
374
|
}
|
|
375
375
|
}
|
|
376
|
-
// Auto-execute
|
|
377
|
-
if (arbPositions < params.max_positions
|
|
378
|
-
const best
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
376
|
+
// Auto-execute: try each opportunity until one succeeds
|
|
377
|
+
if (arbPositions < params.max_positions) {
|
|
378
|
+
for (const best of opportunities) {
|
|
379
|
+
if (best.mode === "spot-perp") {
|
|
380
|
+
const opened = await this.openSpotPerp(best, adapters, ctx);
|
|
381
|
+
if (opened)
|
|
382
|
+
return [{ type: "noop" }];
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
383
385
|
const longAdapter = adapters.get(best.longExchange);
|
|
384
386
|
const shortAdapter = adapters.get(best.shortExchange);
|
|
385
|
-
if (longAdapter
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
387
|
+
if (!longAdapter || !shortAdapter)
|
|
388
|
+
continue;
|
|
389
|
+
const price = ratesByExchange.get(best.longExchange)?.get(best.symbol)?.price ?? 0;
|
|
390
|
+
if (price <= 0)
|
|
391
|
+
continue;
|
|
392
|
+
// Balance check
|
|
393
|
+
let targetSizeUsd = params.size_usd;
|
|
394
|
+
try {
|
|
395
|
+
const [longBal, shortBal] = await Promise.all([
|
|
396
|
+
longAdapter.getBalance(),
|
|
397
|
+
shortAdapter.getBalance(),
|
|
398
|
+
]);
|
|
399
|
+
const longAvail = parseFloat(longBal.available);
|
|
400
|
+
const shortAvail = parseFloat(shortBal.available);
|
|
401
|
+
const minAvail = Math.min(longAvail, shortAvail);
|
|
402
|
+
const maxAffordable = minAvail * 0.8;
|
|
403
|
+
if (maxAffordable < 10) {
|
|
404
|
+
ctx.log(` [ARB] Skip ${best.symbol}: insufficient balance ($${minAvail.toFixed(2)} available)`);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
if (targetSizeUsd > maxAffordable) {
|
|
408
|
+
ctx.log(` [ARB] Size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance $${minAvail.toFixed(2)})`);
|
|
409
|
+
targetSizeUsd = Math.floor(maxAffordable);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch { /* non-critical, proceed with original size */ }
|
|
413
|
+
// Liquidity check
|
|
414
|
+
const liq = await checkArbLiquidity(longAdapter, shortAdapter, best.symbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
|
|
415
|
+
if (!liq.viable)
|
|
416
|
+
continue;
|
|
417
|
+
// Compute matched size
|
|
418
|
+
let matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange);
|
|
419
|
+
if (!matched) {
|
|
420
|
+
ctx.log(` [ARB] Skip ${best.symbol}: can't compute matched size (min notional or precision issue)`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
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)}%)`);
|
|
425
|
+
try {
|
|
426
|
+
await Promise.all([
|
|
427
|
+
longAdapter.marketOrder(best.symbol, "buy", matched.size),
|
|
428
|
+
shortAdapter.marketOrder(best.symbol, "sell", matched.size),
|
|
429
|
+
]);
|
|
430
|
+
}
|
|
431
|
+
catch (orderErr) {
|
|
432
|
+
const errMsg = orderErr instanceof Error ? orderErr.message : String(orderErr);
|
|
433
|
+
const lotMatch = errMsg.match(/not a multiple of lot size (\d+(?:\.\d+)?)/);
|
|
434
|
+
if (lotMatch) {
|
|
435
|
+
const lotSize = parseFloat(lotMatch[1]);
|
|
436
|
+
ctx.log(` [ARB] Lot size ${lotSize} detected, recalculating...`);
|
|
437
|
+
matched = computeMatchedSize(liq.adjustedSizeUsd, price, best.longExchange, best.shortExchange, { lotSize });
|
|
438
|
+
if (!matched) {
|
|
439
|
+
ctx.log(` [ARB] Skip ${best.symbol}: can't meet lot size ${lotSize}`);
|
|
440
|
+
continue;
|
|
406
441
|
}
|
|
442
|
+
ctx.log(` [ARB] Retry: ${matched.size} ${best.symbol} ($${matched.notional.toFixed(0)}/leg)`);
|
|
443
|
+
await Promise.all([
|
|
444
|
+
longAdapter.marketOrder(best.symbol, "buy", matched.size),
|
|
445
|
+
shortAdapter.marketOrder(best.symbol, "sell", matched.size),
|
|
446
|
+
]);
|
|
407
447
|
}
|
|
408
|
-
|
|
409
|
-
|
|
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" }];
|
|
448
|
+
else {
|
|
449
|
+
throw orderErr;
|
|
474
450
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
451
|
+
}
|
|
452
|
+
// Verify fills match
|
|
453
|
+
try {
|
|
454
|
+
const recon = await reconcileArbFills(longAdapter, shortAdapter, best.symbol, (msg) => ctx.log(` ${msg}`));
|
|
455
|
+
if (!recon.matched) {
|
|
456
|
+
ctx.log(` [ARB] WARNING: fills not matched after correction attempt`);
|
|
478
457
|
}
|
|
479
458
|
}
|
|
459
|
+
catch (reconErr) {
|
|
460
|
+
const reconMsg = reconErr instanceof Error ? reconErr.message : String(reconErr);
|
|
461
|
+
ctx.log(` [ARB] WARNING: Fill reconciliation failed: ${reconMsg}`);
|
|
462
|
+
}
|
|
463
|
+
// Track position
|
|
464
|
+
const currentPositions = ctx.state.get("arbOpenPositions");
|
|
465
|
+
currentPositions.push({
|
|
466
|
+
symbol: best.symbol,
|
|
467
|
+
longExchange: best.longExchange,
|
|
468
|
+
shortExchange: best.shortExchange,
|
|
469
|
+
entrySpread: best.spread,
|
|
470
|
+
size: matched.size,
|
|
471
|
+
mode: "perp-perp",
|
|
472
|
+
});
|
|
473
|
+
ctx.state.set("arbOpenPositions", currentPositions);
|
|
474
|
+
ctx.state.set("arbPositions", currentPositions.length);
|
|
475
|
+
ctx.log(` [ARB] Position opened! (${currentPositions.length}/${params.max_positions})`);
|
|
476
|
+
return [{ type: "noop" }];
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
480
|
+
ctx.log(` [ARB] Execution failed for ${best.symbol}: ${msg}`);
|
|
481
|
+
continue;
|
|
480
482
|
}
|
|
481
483
|
}
|
|
482
484
|
}
|
|
@@ -593,13 +595,13 @@ export class FundingArbStrategy {
|
|
|
593
595
|
const perpAdapter = adapters.get(opp.shortExchange) ?? adapters.get(exchangeName);
|
|
594
596
|
if (!perpAdapter) {
|
|
595
597
|
ctx.log(` [ARB] Open spot-perp failed: no adapter for ${opp.shortExchange}`);
|
|
596
|
-
return;
|
|
598
|
+
return false;
|
|
597
599
|
}
|
|
598
600
|
try {
|
|
599
601
|
const spotAdapter = await this.getSpotAdapter(exchangeName, perpAdapter);
|
|
600
602
|
if (!spotAdapter) {
|
|
601
603
|
ctx.log(` [ARB] Open spot-perp failed: spot adapter not available for ${exchangeName}`);
|
|
602
|
-
return;
|
|
604
|
+
return false;
|
|
603
605
|
}
|
|
604
606
|
// Balance check: need full notional for spot + margin for perp
|
|
605
607
|
let targetSizeUsd = params.size_usd;
|
|
@@ -609,7 +611,7 @@ export class FundingArbStrategy {
|
|
|
609
611
|
const maxAffordable = perpAvail * 0.8;
|
|
610
612
|
if (maxAffordable < 10) {
|
|
611
613
|
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: insufficient balance ($${perpAvail.toFixed(2)} available)`);
|
|
612
|
-
return;
|
|
614
|
+
return false;
|
|
613
615
|
}
|
|
614
616
|
if (targetSizeUsd > maxAffordable) {
|
|
615
617
|
ctx.log(` [ARB] Spot-perp size reduced $${targetSizeUsd} → $${maxAffordable.toFixed(0)} (limited by balance)`);
|
|
@@ -621,12 +623,12 @@ export class FundingArbStrategy {
|
|
|
621
623
|
const priceEstimate = await this.getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
|
|
622
624
|
if (priceEstimate <= 0) {
|
|
623
625
|
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't determine price`);
|
|
624
|
-
return;
|
|
626
|
+
return false;
|
|
625
627
|
}
|
|
626
628
|
// Liquidity check
|
|
627
629
|
const liq = await checkSpotPerpLiquidity(spotAdapter, perpAdapter, opp.symbol, perpSymbol, targetSizeUsd, 0.5, (msg) => ctx.log(` ${msg}`));
|
|
628
630
|
if (!liq.viable)
|
|
629
|
-
return;
|
|
631
|
+
return false;
|
|
630
632
|
// Compute matched size
|
|
631
633
|
const spotMarkets = await spotAdapter.getSpotMarkets();
|
|
632
634
|
const spotMarket = spotMarkets.find(m => m.baseToken.toUpperCase() === opp.symbol.toUpperCase());
|
|
@@ -634,7 +636,7 @@ export class FundingArbStrategy {
|
|
|
634
636
|
const matched = computeSpotPerpMatchedSize(liq.adjustedSizeUsd, priceEstimate, exchangeName, opp.shortExchange, spotDecimals);
|
|
635
637
|
if (!matched) {
|
|
636
638
|
ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: can't compute matched size`);
|
|
637
|
-
return;
|
|
639
|
+
return false;
|
|
638
640
|
}
|
|
639
641
|
ctx.log(` [ARB] Opening spot-perp: ${matched.size} ${opp.symbol} ($${matched.notional.toFixed(0)}, slippage ~${liq.spotSlippage.toFixed(2)}%/${liq.perpSlippage.toFixed(2)}%)`);
|
|
640
642
|
// Step 1: Transfer USDC to spot
|
|
@@ -660,10 +662,12 @@ export class FundingArbStrategy {
|
|
|
660
662
|
ctx.state.set("arbOpenPositions", currentPositions);
|
|
661
663
|
ctx.state.set("arbPositions", currentPositions.length);
|
|
662
664
|
ctx.log(` [ARB] Spot-perp position opened! (${currentPositions.length}/${params.max_positions})`);
|
|
665
|
+
return true;
|
|
663
666
|
}
|
|
664
667
|
catch (err) {
|
|
665
668
|
const msg = err instanceof Error ? err.message : String(err);
|
|
666
669
|
ctx.log(` [ARB] Open spot-perp failed for ${opp.symbol}: ${msg}`);
|
|
670
|
+
return false;
|
|
667
671
|
}
|
|
668
672
|
}
|
|
669
673
|
/** Transfer USDC from perp to spot account (exchange-specific) */
|
|
@@ -760,7 +764,9 @@ export class FundingArbStrategy {
|
|
|
760
764
|
async fetchRates(adapter, _exchangeName) {
|
|
761
765
|
try {
|
|
762
766
|
const markets = await adapter.getMarkets();
|
|
763
|
-
return markets
|
|
767
|
+
return markets
|
|
768
|
+
.filter(m => m.fundingRate != null) // skip symbols with unavailable rates
|
|
769
|
+
.map(m => ({
|
|
764
770
|
symbol: m.symbol,
|
|
765
771
|
rate: parseFloat(m.fundingRate),
|
|
766
772
|
price: parseFloat(m.markPrice),
|
|
@@ -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
|
});
|
package/dist/commands/market.js
CHANGED
|
@@ -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`);
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -79,7 +79,7 @@ export class AsterAdapter {
|
|
|
79
79
|
symbol: this._fromApi(sym),
|
|
80
80
|
markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
|
|
81
81
|
indexPrice: String(premium?.indexPrice ?? "0"),
|
|
82
|
-
fundingRate: String(premium
|
|
82
|
+
fundingRate: premium?.lastFundingRate != null ? String(premium.lastFundingRate) : null,
|
|
83
83
|
volume24h: String(ticker?.quoteVolume ?? ticker?.volume ?? "0"),
|
|
84
84
|
openInterest: "0",
|
|
85
85
|
maxLeverage: maxLev,
|
|
@@ -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
|
|
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),
|
|
@@ -266,18 +266,22 @@ export class LighterAdapter {
|
|
|
266
266
|
return this._marketsCache;
|
|
267
267
|
const markets = [];
|
|
268
268
|
try {
|
|
269
|
-
|
|
270
|
-
|
|
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:
|
|
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,
|