perp-cli 0.7.43 → 0.7.47

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.
@@ -26,10 +26,13 @@ export declare class FundingArbStrategy implements Strategy {
26
26
  private get params();
27
27
  private _config;
28
28
  private _failCooldown;
29
+ private _spotAdapterCache;
29
30
  init(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<void>;
30
31
  onTick(ctx: StrategyContext, _snapshot: EnrichedSnapshot): Promise<StrategyAction[]>;
31
32
  onStop(ctx: StrategyContext): Promise<StrategyAction[]>;
32
- /** Close a spot-perp position: sell spot, transfer USDC back, close perp short */
33
+ /** Close a spot-perp position: sell spot, transfer USDC back, close perp short.
34
+ * Returns true only if ALL steps succeed. If spot sell succeeds but perp close fails,
35
+ * attempts to re-buy spot as rollback and returns false. */
33
36
  private closeSpotPerp;
34
37
  /** Open a spot-perp position: transfer USDC to spot, buy spot, short perp */
35
38
  private openSpotPerp;
@@ -25,6 +25,7 @@ export class FundingArbStrategy {
25
25
  }
26
26
  _config = {};
27
27
  _failCooldown = new Map(); // "symbol:action" → timestamp of next retry
28
+ _spotAdapterCache = new Map();
28
29
  async init(ctx, _snapshot) {
29
30
  // Prune old execution logs (keep 30 days)
30
31
  try {
@@ -168,6 +169,7 @@ export class FundingArbStrategy {
168
169
  ctx.state.set("fundingIncome", historicalFunding);
169
170
  ctx.log(` [FUND] Total historical funding: $${historicalFunding.toFixed(4)}`);
170
171
  }
172
+ // Note: aster funding intervals are bootstrapped lazily via getFundingHours()
171
173
  }
172
174
  catch (err) {
173
175
  const msg = err instanceof Error ? err.message : String(err);
@@ -200,15 +202,17 @@ export class FundingArbStrategy {
200
202
  const marginUsed = parseFloat(bal.marginUsed);
201
203
  const marginPct = equity > 0 ? (marginUsed / equity) * 100 : 100;
202
204
  exchangeBalances.set(name, { equity, available: parseFloat(bal.available), marginPct });
203
- if (marginPct < 70) {
205
+ if (marginPct < 90) {
204
206
  availableAdapters.set(name, a);
205
207
  }
206
208
  else {
207
209
  ctx.log(` [ARB] ${name} margin ${marginPct.toFixed(0)}% — skipping for this tick`);
208
210
  }
209
211
  }
210
- catch {
211
- availableAdapters.set(name, a); // on error, include it
212
+ catch (err) {
213
+ const msg = err instanceof Error ? err.message : String(err);
214
+ ctx.log(` [ARB] getBalance failed for ${name}: ${msg} — skipping`);
215
+ // Don't include in availableAdapters
212
216
  }
213
217
  }
214
218
  // Determine which exchanges need rate fetching.
@@ -322,8 +326,9 @@ export class FundingArbStrategy {
322
326
  const sign = signedSpread >= 0 ? "+" : "";
323
327
  ctx.log(` [ARB] Closing ${pos.symbol}: signed spread ${sign}${signedSpread.toFixed(1)}% (close_spread=${params.close_spread}%, entry=${pos.entrySpread.toFixed(1)}%)`);
324
328
  if (pos.mode === "spot-perp") {
325
- await this.closeSpotPerp(pos, adapters, ctx);
326
- toClose.push(pos);
329
+ const spotPerpClosed = await this.closeSpotPerp(pos, adapters, ctx);
330
+ if (spotPerpClosed)
331
+ toClose.push(pos);
327
332
  }
328
333
  else {
329
334
  const longAdapter = adapters.get(pos.longExchange);
@@ -510,12 +515,23 @@ export class FundingArbStrategy {
510
515
  const leverage = Math.min(configLeverage, longMaxLev, shortMaxLev);
511
516
  // Set leverage on both exchanges before ordering
512
517
  try {
513
- await Promise.all([
514
- longAdapter.setLeverage(best.symbol, leverage, "cross"),
515
- shortAdapter.setLeverage(best.symbol, leverage, "cross"),
516
- ]);
518
+ await longAdapter.setLeverage(best.symbol, leverage, "cross");
519
+ }
520
+ catch (err) {
521
+ const msg = err instanceof Error ? err.message : String(err);
522
+ ctx.log(` [ARB] setLeverage failed on ${best.longExchange} for ${best.symbol}: ${msg}`);
523
+ this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
524
+ continue;
525
+ }
526
+ try {
527
+ await shortAdapter.setLeverage(best.symbol, leverage, "cross");
528
+ }
529
+ catch (err) {
530
+ const msg = err instanceof Error ? err.message : String(err);
531
+ ctx.log(` [ARB] setLeverage failed on ${best.shortExchange} for ${best.symbol}: ${msg}`);
532
+ this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
533
+ continue;
517
534
  }
518
- catch { /* non-critical, some exchanges set leverage on order */ }
519
535
  // Balance + margin check (use pre-fetched balances from tick start)
520
536
  let targetSizeUsd = params.size_usd;
521
537
  const longBal = exchangeBalances.get(best.longExchange);
@@ -579,6 +595,20 @@ export class FundingArbStrategy {
579
595
  const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
580
596
  ctx.log(` [ARB] CRITICAL: Rollback failed for ${best.symbol}: ${rbMsg}`);
581
597
  logExecution({ type: "multi_leg_rollback", exchange: best.longExchange, symbol: best.symbol, side: "sell", size: matched.size, status: "failed", error: rbMsg, dryRun: false });
598
+ // Track orphaned long position for manual cleanup
599
+ const currentPositions = ctx.state.get("arbOpenPositions");
600
+ currentPositions.push({
601
+ symbol: best.symbol,
602
+ longExchange: best.longExchange,
603
+ shortExchange: best.shortExchange, // note: short leg doesn't exist
604
+ entrySpread: 0,
605
+ size: matched.size,
606
+ mode: "perp-perp",
607
+ entryTime: 0, // flag as orphaned
608
+ });
609
+ ctx.state.set("arbOpenPositions", currentPositions);
610
+ ctx.state.set("arbPositions", currentPositions.length);
611
+ ctx.log(` [ARB] Tracking orphaned long position for manual cleanup`);
582
612
  }
583
613
  this._failCooldown.set(`${best.symbol}:entry`, Date.now() + 5 * 60 * 1000);
584
614
  continue;
@@ -674,38 +704,70 @@ export class FundingArbStrategy {
674
704
  for (const [name, a] of extraAdapters)
675
705
  adapters.set(name, a);
676
706
  }
707
+ const remainingPositions = [];
677
708
  for (const pos of openPositions) {
678
709
  if (pos.mode === "spot-perp") {
679
- await this.closeSpotPerp(pos, adapters, ctx);
710
+ const closed = await this.closeSpotPerp(pos, adapters, ctx);
711
+ if (!closed) {
712
+ ctx.log(` [ARB] CRITICAL: spot-perp ${pos.symbol} could not be fully closed on stop — requires manual cleanup`);
713
+ remainingPositions.push(pos);
714
+ }
680
715
  }
681
716
  else {
682
717
  const longAdapter = adapters.get(pos.longExchange);
683
718
  const shortAdapter = adapters.get(pos.shortExchange);
684
719
  if (longAdapter && shortAdapter) {
685
720
  ctx.log(` [ARB] Closing ${pos.symbol} on stop (${pos.size})`);
721
+ let closeLongOk = false;
686
722
  try {
687
723
  const closeLongResult = await longAdapter.marketOrder(pos.symbol, "sell", pos.size, { reduceOnly: true });
688
724
  logExecution({ type: "arb_close", exchange: pos.longExchange, symbol: pos.symbol, side: "sell", size: pos.size, status: "success", dryRun: false, meta: { mode: pos.mode, leg: "long", stop: true, response: closeLongResult } });
725
+ closeLongOk = true;
689
726
  }
690
727
  catch (err) {
691
728
  const msg = err instanceof Error ? err.message : String(err);
692
729
  ctx.log(` [ARB] Close on stop failed (long) for ${pos.symbol}: ${msg}`);
693
730
  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", stop: true } });
694
731
  }
732
+ if (!closeLongOk) {
733
+ ctx.log(` [ARB] CRITICAL: ${pos.symbol} long close failed on stop — skipping short close to preserve hedge`);
734
+ remainingPositions.push(pos);
735
+ continue;
736
+ }
737
+ let closeShortOk = false;
695
738
  try {
696
739
  const closeShortResult = await shortAdapter.marketOrder(pos.symbol, "buy", pos.size, { reduceOnly: true });
697
740
  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", stop: true, response: closeShortResult } });
741
+ closeShortOk = true;
698
742
  }
699
743
  catch (err) {
700
744
  const msg = err instanceof Error ? err.message : String(err);
701
- ctx.log(` [ARB] Close on stop failed (short) for ${pos.symbol}: ${msg}`);
745
+ ctx.log(` [ARB] Close on stop failed (short) for ${pos.symbol}: ${msg} — re-opening long to restore hedge`);
702
746
  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", stop: true } });
747
+ // Rollback: re-open long to restore hedge
748
+ try {
749
+ await longAdapter.marketOrder(pos.symbol, "buy", pos.size);
750
+ ctx.log(` [ARB] Rollback: re-opened long ${pos.size} ${pos.symbol} on ${pos.longExchange}`);
751
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "success", dryRun: false });
752
+ }
753
+ catch (rbErr) {
754
+ const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
755
+ ctx.log(` [ARB] CRITICAL: Stop rollback failed for ${pos.symbol}: ${rbMsg}`);
756
+ logExecution({ type: "multi_leg_rollback", exchange: pos.longExchange, symbol: pos.symbol, side: "buy", size: pos.size, status: "failed", error: rbMsg, dryRun: false });
757
+ }
703
758
  }
759
+ if (!closeShortOk) {
760
+ remainingPositions.push(pos);
761
+ }
762
+ }
763
+ else {
764
+ ctx.log(` [ARB] CRITICAL: ${pos.symbol} adapters not found on stop — requires manual cleanup`);
765
+ remainingPositions.push(pos);
704
766
  }
705
767
  }
706
768
  }
707
- ctx.state.set("arbOpenPositions", []);
708
- ctx.state.set("arbPositions", 0);
769
+ ctx.state.set("arbOpenPositions", remainingPositions);
770
+ ctx.state.set("arbPositions", remainingPositions.length);
709
771
  }
710
772
  // Cancel on extra adapters directly
711
773
  if (extraAdapters) {
@@ -718,29 +780,34 @@ export class FundingArbStrategy {
718
780
  }
719
781
  return [{ type: "cancel_all" }];
720
782
  }
721
- /** Close a spot-perp position: sell spot, transfer USDC back, close perp short */
783
+ /** Close a spot-perp position: sell spot, transfer USDC back, close perp short.
784
+ * Returns true only if ALL steps succeed. If spot sell succeeds but perp close fails,
785
+ * attempts to re-buy spot as rollback and returns false. */
722
786
  async closeSpotPerp(pos, adapters, ctx) {
723
787
  const exchangeName = pos.longExchange.replace("-spot", "");
724
788
  const perpAdapter = adapters.get(pos.shortExchange) ?? adapters.get(exchangeName);
725
789
  if (!perpAdapter) {
726
790
  ctx.log(` [ARB] Close spot-perp failed: no adapter for ${pos.shortExchange}`);
727
- return;
791
+ return false;
728
792
  }
729
793
  try {
730
794
  const spotAdapter = await this.getSpotAdapter(exchangeName, perpAdapter);
731
795
  if (!spotAdapter) {
732
796
  ctx.log(` [ARB] Close spot-perp failed: spot adapter not available for ${exchangeName}`);
733
- return;
797
+ return false;
734
798
  }
735
- ctx.log(` [ARB] Closing spot-perp ${pos.symbol} on stop (spot sell + perp buy)`);
799
+ ctx.log(` [ARB] Closing spot-perp ${pos.symbol} (spot sell + perp buy)`);
736
800
  // Step 1: Sell spot
801
+ let spotSold = false;
737
802
  try {
738
803
  await spotAdapter.spotMarketOrder(pos.symbol, "sell", pos.size);
739
804
  ctx.log(` [ARB] Sold spot ${pos.size} ${pos.symbol}`);
805
+ spotSold = true;
740
806
  }
741
807
  catch (err) {
742
808
  const msg = err instanceof Error ? err.message : String(err);
743
809
  ctx.log(` [ARB] Spot sell failed for ${pos.symbol}: ${msg}`);
810
+ return false;
744
811
  }
745
812
  // Step 2: Transfer USDC back to perp (use 95% of notional as estimate)
746
813
  try {
@@ -755,21 +822,36 @@ export class FundingArbStrategy {
755
822
  catch (err) {
756
823
  const msg = err instanceof Error ? err.message : String(err);
757
824
  ctx.log(` [ARB] USDC transfer back failed: ${msg}`);
825
+ // Non-fatal: continue to close perp short
758
826
  }
759
827
  // Step 3: Close perp short
828
+ const perpSymbol = this.getPerpSymbol(pos.symbol, pos.shortExchange);
760
829
  try {
761
- const perpSymbol = this.getPerpSymbol(pos.symbol, pos.shortExchange);
762
830
  await perpAdapter.marketOrder(perpSymbol, "buy", pos.size, { reduceOnly: true });
763
831
  ctx.log(` [ARB] Closed perp short ${pos.size} ${perpSymbol}`);
832
+ return true;
764
833
  }
765
834
  catch (err) {
766
835
  const msg = err instanceof Error ? err.message : String(err);
767
836
  ctx.log(` [ARB] Perp close failed for ${pos.symbol}: ${msg}`);
837
+ // Rollback: re-buy spot to restore hedge
838
+ if (spotSold) {
839
+ try {
840
+ await spotAdapter.spotMarketOrder(pos.symbol, "buy", pos.size);
841
+ ctx.log(` [ARB] Rollback: re-bought spot ${pos.size} ${pos.symbol} on ${exchangeName}`);
842
+ }
843
+ catch (rbErr) {
844
+ const rbMsg = rbErr instanceof Error ? rbErr.message : String(rbErr);
845
+ ctx.log(` [ARB] CRITICAL: Spot-perp close rollback failed for ${pos.symbol}: ${rbMsg}`);
846
+ }
847
+ }
848
+ return false;
768
849
  }
769
850
  }
770
851
  catch (err) {
771
852
  const msg = err instanceof Error ? err.message : String(err);
772
853
  ctx.log(` [ARB] Close spot-perp failed for ${pos.symbol}: ${msg}`);
854
+ return false;
773
855
  }
774
856
  }
775
857
  /** Open a spot-perp position: transfer USDC to spot, buy spot, short perp */
@@ -798,7 +880,11 @@ export class FundingArbStrategy {
798
880
  try {
799
881
  await perpAdapter.setLeverage(this.getPerpSymbol(opp.symbol, opp.shortExchange), leverage, "cross");
800
882
  }
801
- catch { /* non-critical */ }
883
+ catch (err) {
884
+ const msg = err instanceof Error ? err.message : String(err);
885
+ ctx.log(` [ARB] setLeverage failed on ${opp.shortExchange} for ${opp.symbol}: ${msg}`);
886
+ return false;
887
+ }
802
888
  // Balance check: spot needs full notional, perp needs sizeUsd / leverage margin
803
889
  let targetSizeUsd = params.size_usd;
804
890
  try {
@@ -806,7 +892,7 @@ export class FundingArbStrategy {
806
892
  // Check margin usage
807
893
  const perpEquity = parseFloat(perpBal.equity);
808
894
  const perpMarginPct = perpEquity > 0 ? parseFloat(perpBal.marginUsed) / perpEquity * 100 : 0;
809
- if (perpMarginPct > 70) {
895
+ if (perpMarginPct > 90) {
810
896
  ctx.log(` [ARB] Skip spot-perp ${opp.symbol}: perp margin usage too high (${perpMarginPct.toFixed(0)}%)`);
811
897
  return false;
812
898
  }
@@ -825,7 +911,11 @@ export class FundingArbStrategy {
825
911
  targetSizeUsd = Math.floor(maxNotional);
826
912
  }
827
913
  }
828
- catch { /* non-critical */ }
914
+ catch (err) {
915
+ const msg = err instanceof Error ? err.message : String(err);
916
+ ctx.log(` [ARB] Balance check failed for spot-perp ${opp.symbol}: ${msg}`);
917
+ return false;
918
+ }
829
919
  const perpSymbol = this.getPerpSymbol(opp.symbol, opp.shortExchange);
830
920
  const priceEstimate = await this.getPriceEstimate(perpAdapter, perpSymbol, opp.symbol);
831
921
  if (priceEstimate <= 0) {
@@ -953,25 +1043,32 @@ export class FundingArbStrategy {
953
1043
  * Returns null if the exchange doesn't support spot or instantiation fails.
954
1044
  */
955
1045
  async getSpotAdapter(name, adapter) {
1046
+ const cached = this._spotAdapterCache.get(name);
1047
+ if (cached)
1048
+ return cached;
956
1049
  try {
1050
+ let spot = null;
957
1051
  if (name === "hyperliquid") {
958
1052
  const { HyperliquidSpotAdapter } = await import("../../exchanges/hyperliquid-spot.js");
959
1053
  const { HyperliquidAdapter } = await import("../../exchanges/hyperliquid.js");
960
1054
  if (adapter instanceof HyperliquidAdapter) {
961
- const spot = new HyperliquidSpotAdapter(adapter);
962
- await spot.init();
963
- return spot;
1055
+ const instance = new HyperliquidSpotAdapter(adapter);
1056
+ await instance.init();
1057
+ spot = instance;
964
1058
  }
965
1059
  }
966
1060
  else if (name === "lighter") {
967
1061
  const { LighterSpotAdapter } = await import("../../exchanges/lighter-spot.js");
968
1062
  const { LighterAdapter } = await import("../../exchanges/lighter.js");
969
1063
  if (adapter instanceof LighterAdapter) {
970
- const spot = new LighterSpotAdapter(adapter);
971
- await spot.init();
972
- return spot;
1064
+ const instance = new LighterSpotAdapter(adapter);
1065
+ await instance.init();
1066
+ spot = instance;
973
1067
  }
974
1068
  }
1069
+ if (spot)
1070
+ this._spotAdapterCache.set(name, spot);
1071
+ return spot;
975
1072
  }
976
1073
  catch { /* not supported */ }
977
1074
  return null;
@@ -1011,12 +1108,22 @@ export class FundingArbStrategy {
1011
1108
  return 0;
1012
1109
  }
1013
1110
  }
1014
- async fetchRates(adapter, _exchangeName) {
1111
+ async fetchRates(adapter, exchangeName) {
1015
1112
  try {
1016
1113
  const markets = await adapter.getMarkets();
1017
- return markets
1018
- .filter(m => m.fundingRate != null)
1019
- .map(m => ({
1114
+ const withRates = markets.filter(m => m.fundingRate != null);
1115
+ // Bootstrap aster funding hours lazily (1 API call per unknown symbol, cached permanently)
1116
+ if (exchangeName === "aster" && "getFundingHours" in adapter) {
1117
+ const aster = adapter;
1118
+ // Only bootstrap symbols not yet cached (up to 5 per tick to limit API calls)
1119
+ const uncached = withRates.filter(m => m.fundingHours === undefined || m.fundingHours === 1);
1120
+ const toBootstrap = uncached.slice(0, 5);
1121
+ for (const m of toBootstrap) {
1122
+ const fh = await aster.getFundingHours(m.symbol);
1123
+ m.fundingHours = fh;
1124
+ }
1125
+ }
1126
+ return withRates.map(m => ({
1020
1127
  symbol: m.symbol,
1021
1128
  rate: parseFloat(m.fundingRate),
1022
1129
  price: parseFloat(m.markPrice),
@@ -14,6 +14,7 @@ export declare class AsterAdapter implements ExchangeAdapter {
14
14
  private _testnet;
15
15
  private _marketsCache;
16
16
  private _marketsCacheTime;
17
+ private _fundingHoursCache;
17
18
  private _accountCache;
18
19
  private _positionsCache;
19
20
  private _ordersCache;
@@ -26,6 +27,8 @@ export declare class AsterAdapter implements ExchangeAdapter {
26
27
  /** Aster API symbol → CLI symbol (ETHUSDT → ETH) */
27
28
  private _fromApi;
28
29
  init(): Promise<void>;
30
+ /** Get funding interval for a symbol (lazy bootstrap — queries on first access, cached permanently). */
31
+ getFundingHours(symbol: string): Promise<number>;
29
32
  getMarkets(): Promise<ExchangeMarketInfo[]>;
30
33
  getOrderbook(symbol: string): Promise<{
31
34
  bids: [string, string][];
@@ -14,6 +14,7 @@ export class AsterAdapter {
14
14
  _testnet;
15
15
  _marketsCache = null;
16
16
  _marketsCacheTime = 0;
17
+ _fundingHoursCache = new Map();
17
18
  _accountCache = null;
18
19
  _positionsCache = null;
19
20
  _ordersCache = null;
@@ -46,6 +47,26 @@ export class AsterAdapter {
46
47
  await this._publicGet("/fapi/v1/time");
47
48
  }
48
49
  // ── Market Data ──
50
+ /** Get funding interval for a symbol (lazy bootstrap — queries on first access, cached permanently). */
51
+ async getFundingHours(symbol) {
52
+ const key = symbol.toUpperCase();
53
+ const cached = this._fundingHoursCache.get(key);
54
+ if (cached !== undefined)
55
+ return cached;
56
+ try {
57
+ const apiSym = this._toApi(key);
58
+ const data = await this._publicGet("/fapi/v1/fundingRate", { symbol: apiSym, limit: "2" });
59
+ if (Array.isArray(data) && data.length >= 2) {
60
+ const hours = Math.abs(data[1].fundingTime - data[0].fundingTime) / 3600000;
61
+ const rounded = hours <= 1.5 ? 1 : hours <= 5 ? 4 : 8;
62
+ this._fundingHoursCache.set(key, rounded);
63
+ return rounded;
64
+ }
65
+ }
66
+ catch { /* non-critical */ }
67
+ this._fundingHoursCache.set(key, 1); // default
68
+ return 1;
69
+ }
49
70
  async getMarkets() {
50
71
  // Return cached result if fresh
51
72
  if (this._marketsCache && Date.now() - this._marketsCacheTime < AsterAdapter.CACHE_TTL) {
@@ -77,12 +98,9 @@ export class AsterAdapter {
77
98
  const maxLev = Number(s.maxLeverage ?? 50);
78
99
  const lotFilter = s.filters
79
100
  ?.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;
101
+ // Use cached funding interval if available (populated by loadFundingIntervals()),
102
+ // otherwise default to 1h.
103
+ const fundingHours = this._fundingHoursCache.get(this._fromApi(sym)) ?? 1;
86
104
  return {
87
105
  symbol: this._fromApi(sym),
88
106
  markPrice: String(premium?.markPrice ?? ticker?.lastPrice ?? "0"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.43",
3
+ "version": "0.7.47",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",