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.
- package/dist/bot/conditions.js +1 -1
- package/dist/bot/strategies/funding-arb-strategy.js +125 -105
- 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 +5 -1
- package/dist/exchanges/hyperliquid.js +1 -1
- package/dist/exchanges/interface.d.ts +3 -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;
|
|
@@ -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
|
|
377
|
-
if (arbPositions < params.max_positions
|
|
378
|
-
const best
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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" }];
|
|
457
|
+
else {
|
|
458
|
+
throw orderErr;
|
|
474
459
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
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
|
});
|
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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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,
|