perp-cli 0.7.11 → 0.7.13

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.
@@ -152,22 +152,6 @@ mode = "headless") {
152
152
  try {
153
153
  // Get market data (enriched for strategy use)
154
154
  const snapshot = await getEnrichedSnapshot(adapter, config.symbol);
155
- // Update equity
156
- try {
157
- const bal = await adapter.getBalance();
158
- state.equity = parseFloat(bal.equity);
159
- if (state.equity > state.peakEquity)
160
- state.peakEquity = state.equity;
161
- // Reset daily baseline at day boundary
162
- const today = new Date().toISOString().slice(0, 10);
163
- if (today !== state.dailyStartDate) {
164
- state.dailyStartEquity = state.equity;
165
- state.dailyStartDate = today;
166
- }
167
- state.dailyPnl = state.equity - state.dailyStartEquity;
168
- state.totalPnl = state.equity - state.initialEquity;
169
- }
170
- catch { /* non-critical */ }
171
155
  const context = {
172
156
  equity: state.equity,
173
157
  startTime: state.startTime,
@@ -258,18 +242,41 @@ mode = "headless") {
258
242
  effectiveLog(` Exit condition met, returning to monitoring...`);
259
243
  }
260
244
  }
261
- // ── Emit state update for TUI / JSON ──
245
+ // ── Fetch balances + emit state update for TUI / JSON ──
262
246
  const [positions, openOrders, exchangeBalances] = await Promise.all([
263
247
  extraAdapters ? fetchAllPositions(adapter, config.symbol, extraAdapters) : fetchPositions(adapter, config.symbol, adapter.name),
264
248
  fetchOpenOrders(adapter, config.symbol),
265
- extraAdapters ? fetchExchangeBalances(adapter, extraAdapters) : Promise.resolve(undefined),
249
+ fetchExchangeBalances(adapter, extraAdapters),
266
250
  ]);
267
- const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances);
268
- if (mode === "tui") {
269
- tuiEmitter.emitState(tuiState);
251
+ // Update equity using the already-fetched balance (avoids a redundant getBalance() call)
252
+ try {
253
+ const primaryBal = exchangeBalances.find(b => b.exchange === adapter.name);
254
+ if (primaryBal) {
255
+ state.equity = parseFloat(primaryBal.equity);
256
+ if (state.equity > state.peakEquity)
257
+ state.peakEquity = state.equity;
258
+ // Reset daily baseline at day boundary
259
+ const today = new Date().toISOString().slice(0, 10);
260
+ if (today !== state.dailyStartDate) {
261
+ state.dailyStartEquity = state.equity;
262
+ state.dailyStartDate = today;
263
+ }
264
+ state.dailyPnl = state.equity - state.dailyStartEquity;
265
+ state.totalPnl = state.equity - state.initialEquity;
266
+ }
270
267
  }
271
- else if (mode === "json") {
272
- console.log(JSON.stringify(tuiState));
268
+ catch { /* non-critical */ }
269
+ if (mode === "tui" || mode === "json") {
270
+ const tuiState = buildTuiState(state, config, snapshot, positions, openOrders, strategyCtx, exchangeBalances);
271
+ if (mode === "tui") {
272
+ tuiEmitter.emitState(tuiState);
273
+ }
274
+ else {
275
+ console.log(JSON.stringify(tuiState));
276
+ }
277
+ }
278
+ else {
279
+ effectiveLog(` tick: phase=${state.phase} equity=$${state.equity.toFixed(2)}`);
273
280
  }
274
281
  // Update job state
275
282
  if (jobId) {
@@ -26,6 +26,9 @@ export class FundingArbStrategy {
26
26
  ctx.state.set("arbRunning", true);
27
27
  ctx.state.set("arbOpenPositions", []);
28
28
  ctx.state.set("arbPositions", 0);
29
+ ctx.state.set("fundingIncome", 0);
30
+ ctx.state.set("fundingLastCheck", Date.now());
31
+ ctx.state.set("fundingHistory", []);
29
32
  ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | close < ${params.close_spread}% | size $${params.size_usd}`);
30
33
  ctx.log(` [ARB] Exchanges: ${params.exchanges.join(", ")}`);
31
34
  }
@@ -54,6 +57,41 @@ export class FundingArbStrategy {
54
57
  map.set(r.symbol.toUpperCase(), { rate: r.rate, price: r.price });
55
58
  ratesByExchange.set(name, map);
56
59
  }
60
+ // ── Fetch funding income periodically (every 5 minutes) ──
61
+ const lastCheck = ctx.state.get("fundingLastCheck");
62
+ if (openPositions.length > 0 && Date.now() - lastCheck > 5 * 60 * 1000) {
63
+ try {
64
+ let totalFunding = 0;
65
+ for (const [name, a] of adapters) {
66
+ try {
67
+ const payments = await a.getFundingPayments(50);
68
+ const startTime = ctx.state.get("fundingLastCheck");
69
+ const recent = payments.filter(p => p.timestamp > startTime - 5 * 60 * 1000);
70
+ const sum = recent.reduce((s, p) => s + parseFloat(p.amount), 0);
71
+ totalFunding += sum;
72
+ if (Math.abs(sum) > 0.001) {
73
+ ctx.log(` [FUND] ${name}: $${sum.toFixed(4)} funding received`);
74
+ }
75
+ }
76
+ catch { /* not all adapters support this */ }
77
+ }
78
+ const prev = ctx.state.get("fundingIncome");
79
+ ctx.state.set("fundingIncome", prev + totalFunding);
80
+ ctx.state.set("fundingLastCheck", Date.now());
81
+ }
82
+ catch { /* non-critical */ }
83
+ }
84
+ // ── Update strategy state for TUI display ──
85
+ const arbPosForDisplay = openPositions.map(p => {
86
+ const longRate = ratesByExchange.get(p.longExchange)?.get(p.symbol)?.rate;
87
+ const shortRate = ratesByExchange.get(p.shortExchange)?.get(p.symbol)?.rate;
88
+ const spread = (longRate !== undefined && shortRate !== undefined)
89
+ ? computeAnnualSpread(shortRate, p.shortExchange, longRate, p.longExchange)
90
+ : 0;
91
+ return `${p.symbol} ${p.longExchange}↔${p.shortExchange} ${spread.toFixed(1)}%`;
92
+ });
93
+ ctx.state.set("arbPositionDetails", arbPosForDisplay);
94
+ ctx.state.set("fundingTotal", `$${ctx.state.get("fundingIncome").toFixed(4)}`);
57
95
  // ── Check existing positions for close_spread ──
58
96
  const toClose = [];
59
97
  for (const pos of openPositions) {
@@ -12,6 +12,9 @@ export declare class AsterAdapter implements ExchangeAdapter {
12
12
  private _apiSecret;
13
13
  private _baseUrl;
14
14
  private _testnet;
15
+ private _marketsCache;
16
+ private _marketsCacheTime;
17
+ private static readonly CACHE_TTL;
15
18
  constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
16
19
  get isReadOnly(): boolean;
17
20
  /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
@@ -55,6 +58,8 @@ export declare class AsterAdapter implements ExchangeAdapter {
55
58
  routeType?: number;
56
59
  }): Promise<unknown>;
57
60
  private _sign;
61
+ /** Handle 429 rate limit with retry */
62
+ private _handleResponse;
58
63
  private _publicGet;
59
64
  private _signedGet;
60
65
  private _signedPost;
@@ -12,6 +12,9 @@ export class AsterAdapter {
12
12
  _apiSecret;
13
13
  _baseUrl;
14
14
  _testnet;
15
+ _marketsCache = null;
16
+ _marketsCacheTime = 0;
17
+ static CACHE_TTL = 30_000; // 30 seconds
15
18
  constructor(apiKey, apiSecret, testnet = false) {
16
19
  this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
17
20
  this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
@@ -40,6 +43,10 @@ export class AsterAdapter {
40
43
  }
41
44
  // ── Market Data ──
42
45
  async getMarkets() {
46
+ // Return cached result if fresh
47
+ if (this._marketsCache && Date.now() - this._marketsCacheTime < AsterAdapter.CACHE_TTL) {
48
+ return this._marketsCache;
49
+ }
43
50
  const [info, tickers] = await Promise.all([
44
51
  this._publicGet("/fapi/v1/exchangeInfo"),
45
52
  this._publicGet("/fapi/v1/ticker/24hr"),
@@ -57,28 +64,12 @@ export class AsterAdapter {
57
64
  catch { /* non-critical */ }
58
65
  const tradingSymbols = (info?.symbols ?? [])
59
66
  .filter((s) => String(s.contractType) === "PERPETUAL" && String(s.status) === "TRADING");
60
- // Fetch open interest per symbol in parallel (batched to avoid overwhelming the API).
61
- // The /fapi/v1/ticker/24hr response does NOT include openInterest — need a separate endpoint.
62
- const oiMap = new Map();
63
- const OI_BATCH = 20;
64
- for (let i = 0; i < tradingSymbols.length; i += OI_BATCH) {
65
- const batch = tradingSymbols.slice(i, i + OI_BATCH);
66
- const results = await Promise.allSettled(batch.map(async (s) => {
67
- const sym = String(s.symbol);
68
- const res = await this._publicGet("/fapi/v1/openInterest", { symbol: sym });
69
- return { sym, oi: String(res?.openInterest ?? "0") };
70
- }));
71
- for (const r of results) {
72
- if (r.status === "fulfilled")
73
- oiMap.set(r.value.sym, r.value.oi);
74
- }
75
- }
76
- return tradingSymbols
67
+ // OI is fetched lazily per symbol (see getMarketInfo) to avoid 300+ API calls
68
+ const result = tradingSymbols
77
69
  .map((s) => {
78
70
  const sym = String(s.symbol);
79
71
  const ticker = tickerMap.get(sym);
80
72
  const premium = premiumMap.get(sym);
81
- // Extract max leverage from leverage brackets if available
82
73
  const maxLev = Number(s.maxLeverage ?? 50);
83
74
  return {
84
75
  symbol: this._fromApi(sym),
@@ -86,10 +77,13 @@ export class AsterAdapter {
86
77
  indexPrice: String(premium?.indexPrice ?? "0"),
87
78
  fundingRate: String(premium?.lastFundingRate ?? "0"),
88
79
  volume24h: String(ticker?.quoteVolume ?? ticker?.volume ?? "0"),
89
- openInterest: oiMap.get(sym) ?? "0",
80
+ openInterest: "0",
90
81
  maxLeverage: maxLev,
91
82
  };
92
83
  });
84
+ this._marketsCache = result;
85
+ this._marketsCacheTime = Date.now();
86
+ return result;
93
87
  }
94
88
  async getOrderbook(symbol) {
95
89
  const res = await this._publicGet("/fapi/v1/depth", { symbol: this._toApi(symbol), limit: "50" });
@@ -359,62 +353,82 @@ export class AsterAdapter {
359
353
  .digest("hex");
360
354
  return `${queryString}&signature=${signature}`;
361
355
  }
356
+ /** Handle 429 rate limit with retry */
357
+ async _handleResponse(res, method, path, attempt = 0) {
358
+ if (res.status === 429) {
359
+ if (attempt >= 2)
360
+ throw new Error(`${method} ${path} rate limited after ${attempt + 1} attempts`);
361
+ const retryAfter = parseInt(res.headers.get("Retry-After") || "5", 10);
362
+ const waitMs = Math.min(retryAfter * 1000, 30000);
363
+ await new Promise(r => setTimeout(r, waitMs));
364
+ return null; // signal retry
365
+ }
366
+ if (!res.ok) {
367
+ const text = await res.text();
368
+ throw new Error(`${method} ${path} failed (${res.status}): ${text.slice(0, 200)}`);
369
+ }
370
+ return res.json();
371
+ }
362
372
  async _publicGet(path, params = {}) {
363
373
  const qs = Object.entries(params)
364
374
  .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
365
375
  .join("&");
366
376
  const url = `${this._baseUrl}${path}${qs ? `?${qs}` : ""}`;
367
- const res = await fetch(url);
368
- if (!res.ok) {
369
- const text = await res.text();
370
- throw new Error(`GET ${path} failed (${res.status}): ${text.slice(0, 200)}`);
377
+ for (let attempt = 0; attempt < 3; attempt++) {
378
+ const res = await fetch(url);
379
+ const result = await this._handleResponse(res, "GET", path, attempt);
380
+ if (result !== null)
381
+ return result;
371
382
  }
372
- return res.json();
383
+ throw new Error(`GET ${path} failed: max retries exceeded`);
373
384
  }
374
385
  async _signedGet(path, params = {}) {
375
386
  if (this.isReadOnly)
376
387
  throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
377
- const signed = this._sign({ ...params });
378
- const url = `${this._baseUrl}${path}?${signed}`;
379
- const res = await fetch(url, {
380
- headers: { "X-MBX-APIKEY": this._apiKey },
381
- });
382
- if (!res.ok) {
383
- const text = await res.text();
384
- throw new Error(`GET ${path} failed (${res.status}): ${text.slice(0, 200)}`);
388
+ for (let attempt = 0; attempt < 3; attempt++) {
389
+ const signed = this._sign({ ...params });
390
+ const url = `${this._baseUrl}${path}?${signed}`;
391
+ const res = await fetch(url, {
392
+ headers: { "X-MBX-APIKEY": this._apiKey },
393
+ });
394
+ const result = await this._handleResponse(res, "GET", path, attempt);
395
+ if (result !== null)
396
+ return result;
385
397
  }
386
- return res.json();
398
+ throw new Error(`GET ${path} failed: max retries exceeded`);
387
399
  }
388
400
  async _signedPost(path, params = {}) {
389
401
  if (this.isReadOnly)
390
402
  throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
391
- const signed = this._sign({ ...params });
392
- const res = await fetch(`${this._baseUrl}${path}`, {
393
- method: "POST",
394
- headers: {
395
- "X-MBX-APIKEY": this._apiKey,
396
- "Content-Type": "application/x-www-form-urlencoded",
397
- },
398
- body: signed,
399
- });
400
- if (!res.ok) {
401
- const text = await res.text();
402
- throw new Error(`POST ${path} failed (${res.status}): ${text.slice(0, 200)}`);
403
+ for (let attempt = 0; attempt < 3; attempt++) {
404
+ const signed = this._sign({ ...params });
405
+ const res = await fetch(`${this._baseUrl}${path}`, {
406
+ method: "POST",
407
+ headers: {
408
+ "X-MBX-APIKEY": this._apiKey,
409
+ "Content-Type": "application/x-www-form-urlencoded",
410
+ },
411
+ body: signed,
412
+ });
413
+ const result = await this._handleResponse(res, "POST", path, attempt);
414
+ if (result !== null)
415
+ return result;
403
416
  }
404
- return res.json();
417
+ throw new Error(`POST ${path} failed: max retries exceeded`);
405
418
  }
406
419
  async _signedDelete(path, params = {}) {
407
420
  if (this.isReadOnly)
408
421
  throw new Error("No API key configured for Aster. Set ASTER_API_KEY and ASTER_API_SECRET in ~/.perp/.env");
409
- const signed = this._sign({ ...params });
410
- const res = await fetch(`${this._baseUrl}${path}?${signed}`, {
411
- method: "DELETE",
412
- headers: { "X-MBX-APIKEY": this._apiKey },
413
- });
414
- if (!res.ok) {
415
- const text = await res.text();
416
- throw new Error(`DELETE ${path} failed (${res.status}): ${text.slice(0, 200)}`);
422
+ for (let attempt = 0; attempt < 3; attempt++) {
423
+ const signed = this._sign({ ...params });
424
+ const res = await fetch(`${this._baseUrl}${path}?${signed}`, {
425
+ method: "DELETE",
426
+ headers: { "X-MBX-APIKEY": this._apiKey },
427
+ });
428
+ const result = await this._handleResponse(res, "DELETE", path, attempt);
429
+ if (result !== null)
430
+ return result;
417
431
  }
418
- return res.json();
432
+ throw new Error(`DELETE ${path} failed: max retries exceeded`);
419
433
  }
420
434
  }
@@ -16,6 +16,9 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
16
16
  /** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
17
17
  private _dex;
18
18
  private static _lastNonce;
19
+ private _marketsCache;
20
+ private _marketsCacheTime;
21
+ private static readonly CACHE_TTL;
19
22
  constructor(privateKey?: string, testnet?: boolean);
20
23
  /** Set the HIP-3 deployed perp dex to query/trade on. */
21
24
  setDex(dex: string): void;
@@ -44,7 +47,7 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
44
47
  asks: [string, string][];
45
48
  }>;
46
49
  private ensureAddress;
47
- /** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
50
+ /** Read-through cached clearinghouseState — returns cached data if fresh */
48
51
  private _getClearinghouseState;
49
52
  /** Cached spot clearinghouse state — shared between getBalance() and HyperliquidSpotAdapter */
50
53
  _getSpotClearinghouseState(): Promise<Record<string, unknown>>;
@@ -15,7 +15,9 @@ export class HyperliquidAdapter {
15
15
  /** HIP-3 deployed perp dex name. Empty string = native (validator) perps. */
16
16
  _dex = "";
17
17
  static _lastNonce = 0;
18
- // In-memory cache removed — using file-based cache (src/cache.ts) for cross-process dedup
18
+ _marketsCache = null;
19
+ _marketsCacheTime = 0;
20
+ static CACHE_TTL = 30_000;
19
21
  constructor(privateKey, testnet = false) {
20
22
  // Disable WebSocket in SDK — we use REST for all info calls and raw
21
23
  // fetch for /exchange. This avoids the false "Please install ws" warning
@@ -181,6 +183,8 @@ export class HyperliquidAdapter {
181
183
  throw new Error(`Unknown asset: ${symbol}`);
182
184
  }
183
185
  async getMarkets() {
186
+ if (this._marketsCache && Date.now() - this._marketsCacheTime < HyperliquidAdapter.CACHE_TTL)
187
+ return this._marketsCache;
184
188
  let universe;
185
189
  let ctxs;
186
190
  let mids = {};
@@ -207,7 +211,7 @@ export class HyperliquidAdapter {
207
211
  this._assetMapReverse.set(i, sym);
208
212
  });
209
213
  }
210
- return universe.map((asset, i) => {
214
+ const result = universe.map((asset, i) => {
211
215
  const ctx = (ctxs[i] ?? {});
212
216
  const sym = String(asset.name);
213
217
  return {
@@ -220,6 +224,9 @@ export class HyperliquidAdapter {
220
224
  maxLeverage: Number(asset.maxLeverage ?? 50),
221
225
  };
222
226
  });
227
+ this._marketsCache = result;
228
+ this._marketsCacheTime = Date.now();
229
+ return result;
223
230
  }
224
231
  async getOrderbook(symbol) {
225
232
  let levels;
@@ -256,12 +263,12 @@ export class HyperliquidAdapter {
256
263
  throw new Error("No private key configured — account data unavailable. Run: perp setup");
257
264
  }
258
265
  }
259
- /** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
266
+ /** Read-through cached clearinghouseState — returns cached data if fresh */
260
267
  async _getClearinghouseState() {
261
268
  this.ensureAddress();
262
- const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
269
+ const { withCache, TTL_ACCOUNT } = await import("../cache.js");
263
270
  const key = this._dex ? `acct:hl:chs:${this._address}:${this._dex}` : `acct:hl:chs:${this._address}`;
264
- return fetchAndCache(key, TTL_ACCOUNT, async () => {
271
+ return withCache(key, TTL_ACCOUNT, async () => {
265
272
  const state = this._dex
266
273
  ? await this._infoPost({ type: "clearinghouseState", user: this._address, dex: this._dex })
267
274
  : await this.sdk.info.perpetuals.getClearinghouseState(this._address);
@@ -20,6 +20,9 @@ export declare class LighterAdapter implements ExchangeAdapter {
20
20
  private _testnet;
21
21
  private _readOnly;
22
22
  private _evmSigner?;
23
+ private _marketsCache;
24
+ private _marketsCacheTime;
25
+ private static readonly CACHE_TTL;
23
26
  /**
24
27
  * @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
25
28
  * @param testnet Use testnet (chain ID 300) instead of mainnet (chain ID 304)
@@ -18,7 +18,9 @@ export class LighterAdapter {
18
18
  _testnet;
19
19
  _readOnly;
20
20
  _evmSigner;
21
- // In-memory cache removed — using file-based cache (src/cache.ts) for cross-process dedup
21
+ _marketsCache = null;
22
+ _marketsCacheTime = 0;
23
+ static CACHE_TTL = 30_000;
22
24
  /**
23
25
  * @param evmKey EVM private key (0x-prefixed, 32 bytes) — for deposits & key registration
24
26
  * @param testnet Use testnet (chain ID 300) instead of mainnet (chain ID 304)
@@ -260,6 +262,8 @@ export class LighterAdapter {
260
262
  return { baseAmount, priceTicks };
261
263
  }
262
264
  async getMarkets() {
265
+ if (this._marketsCache && Date.now() - this._marketsCacheTime < LighterAdapter.CACHE_TTL)
266
+ return this._marketsCache;
263
267
  const markets = [];
264
268
  try {
265
269
  const res = await this.restGet("/orderBookDetails", {});
@@ -281,13 +285,15 @@ export class LighterAdapter {
281
285
  }
282
286
  }
283
287
  catch { /* non-critical */ }
288
+ this._marketsCache = markets;
289
+ this._marketsCacheTime = Date.now();
284
290
  return markets;
285
291
  }
286
292
  async fetchAccount() {
287
293
  if (!this._address)
288
294
  return null;
289
- const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
290
- return fetchAndCache(`acct:lt:account:${this._address}`, TTL_ACCOUNT, async () => {
295
+ const { withCache, TTL_ACCOUNT } = await import("../cache.js");
296
+ return withCache(`acct:lt:account:${this._address}`, TTL_ACCOUNT, async () => {
291
297
  const res = await this.restGet("/account", {
292
298
  by: "l1_address",
293
299
  value: this._address,
@@ -11,6 +11,9 @@ export declare class PacificaAdapter implements ExchangeAdapter {
11
11
  private _hasRealKey;
12
12
  private account;
13
13
  private signMessage;
14
+ private _marketsCache;
15
+ private _marketsCacheTime;
16
+ private static readonly CACHE_TTL;
14
17
  constructor(keypair: Keypair, network?: Network, builderCode?: string, hasRealKey?: boolean);
15
18
  private ensureSigner;
16
19
  /** Inject an external Solana signer. */
@@ -9,7 +9,9 @@ export class PacificaAdapter {
9
9
  _hasRealKey;
10
10
  account;
11
11
  signMessage;
12
- // In-memory caches removed — using file-based cache (src/cache.ts) for cross-process dedup
12
+ _marketsCache = null;
13
+ _marketsCacheTime = 0;
14
+ static CACHE_TTL = 30_000;
13
15
  constructor(keypair, network = "mainnet", builderCode, hasRealKey = true) {
14
16
  this._solanaSigner = new LocalSolanaSigner(keypair);
15
17
  this._hasRealKey = hasRealKey;
@@ -36,12 +38,12 @@ export class PacificaAdapter {
36
38
  throw new Error("keypair not available with external signer");
37
39
  }
38
40
  async _getPrices() {
39
- const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
40
- return fetchAndCache(`acct:pac:prices:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPrices());
41
+ const { withCache, TTL_ACCOUNT } = await import("../cache.js");
42
+ return withCache(`acct:pac:prices:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPrices());
41
43
  }
42
44
  async _getPositions() {
43
- const { fetchAndCache, TTL_ACCOUNT } = await import("../cache.js");
44
- return fetchAndCache(`acct:pac:positions:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPositions(this.account));
45
+ const { withCache, TTL_ACCOUNT } = await import("../cache.js");
46
+ return withCache(`acct:pac:positions:${this.account.slice(0, 8)}`, TTL_ACCOUNT, () => this.client.getPositions(this.account));
45
47
  }
46
48
  get publicKey() {
47
49
  return this.account;
@@ -53,12 +55,14 @@ export class PacificaAdapter {
53
55
  return this.signMessage;
54
56
  }
55
57
  async getMarkets() {
58
+ if (this._marketsCache && Date.now() - this._marketsCacheTime < PacificaAdapter.CACHE_TTL)
59
+ return this._marketsCache;
56
60
  const [markets, prices] = await Promise.all([
57
61
  this.client.getInfo(),
58
62
  this._getPrices(),
59
63
  ]);
60
64
  const priceMap = new Map(prices.map((p) => [p.symbol, p]));
61
- return markets.map((m) => {
65
+ const result = markets.map((m) => {
62
66
  const p = priceMap.get(m.symbol);
63
67
  return {
64
68
  symbol: m.symbol,
@@ -70,6 +74,9 @@ export class PacificaAdapter {
70
74
  maxLeverage: m.max_leverage,
71
75
  };
72
76
  });
77
+ this._marketsCache = result;
78
+ this._marketsCacheTime = Date.now();
79
+ return result;
73
80
  }
74
81
  async getOrderbook(symbol) {
75
82
  const book = await this.client.getBook(symbol);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.11",
3
+ "version": "0.7.13",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",