perp-cli 0.7.12 → 0.7.15

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
+ }
267
+ }
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
+ }
270
277
  }
271
- else if (mode === "json") {
272
- console.log(JSON.stringify(tuiState));
278
+ else {
279
+ effectiveLog(` tick: phase=${state.phase} equity=$${state.equity.toFixed(2)}`);
273
280
  }
274
281
  // Update job state
275
282
  if (jobId) {
@@ -447,6 +454,60 @@ async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
447
454
  }));
448
455
  return results;
449
456
  }
457
+ async function fetchSpotPositions(adapter) {
458
+ const name = adapter.name.toLowerCase();
459
+ if (name !== "hyperliquid" && name !== "lighter")
460
+ return [];
461
+ try {
462
+ if (name === "hyperliquid") {
463
+ const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
464
+ const { HyperliquidAdapter } = await import("../exchanges/hyperliquid.js");
465
+ if (!(adapter instanceof HyperliquidAdapter))
466
+ return [];
467
+ const hlSpot = new HyperliquidSpotAdapter(adapter);
468
+ await hlSpot.init();
469
+ const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
470
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), m.markPrice]));
471
+ const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
472
+ const hlPositions = [];
473
+ for (const b of raw) {
474
+ if (Number(b.total) <= 0)
475
+ continue;
476
+ const base = strip(b.token);
477
+ const markPrice = priceMap.get(base) ?? "0";
478
+ if (base === "USDC" || markPrice === "0")
479
+ continue;
480
+ hlPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
481
+ }
482
+ return hlPositions;
483
+ }
484
+ if (name === "lighter") {
485
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
486
+ const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
487
+ if (!(adapter instanceof LighterAdapter))
488
+ return [];
489
+ const ltSpot = new LighterSpotAdapter(adapter);
490
+ await ltSpot.init();
491
+ const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
492
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), m.markPrice]));
493
+ const ltPositions = [];
494
+ for (const b of raw) {
495
+ if (Number(b.total) <= 0)
496
+ continue;
497
+ const base = b.token.toUpperCase();
498
+ const markPrice = priceMap.get(base) ?? "0";
499
+ if (base === "USDC" || base === "USDC_SPOT" || markPrice === "0")
500
+ continue;
501
+ ltPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
502
+ }
503
+ return ltPositions;
504
+ }
505
+ }
506
+ catch {
507
+ // Spot adapter unavailable — skip silently
508
+ }
509
+ return [];
510
+ }
450
511
  async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
451
512
  const adapters = [[primaryAdapter.name, primaryAdapter]];
452
513
  if (extraAdapters) {
@@ -455,8 +516,11 @@ async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
455
516
  }
456
517
  const all = [];
457
518
  await Promise.all(adapters.map(async ([name, a]) => {
458
- const pos = await fetchPositions(a, symbol, name);
459
- all.push(pos);
519
+ const [perpPos, spotPos] = await Promise.all([
520
+ fetchPositions(a, symbol, name),
521
+ fetchSpotPositions(a),
522
+ ]);
523
+ all.push([...perpPos, ...spotPos]);
460
524
  }));
461
525
  return all.flat();
462
526
  }
@@ -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) {
@@ -1,4 +1,4 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState, useEffect, useCallback } from "react";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
4
  // ── Helpers ──
@@ -73,7 +73,17 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
73
73
  return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", marginBottom: 1, children: [_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, color: "cyan", children: [state.strategy, " | ", state.symbol, " | ", state.exchange] }), _jsxs(Text, { color: "gray", children: ["Runtime: ", formatDuration(state.runtime), " | Tick: ", state.tick] })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: phaseColor(state.phase), children: phaseIndicator(state.phase) }) })] }), isMultiSymbol ? (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Exchanges: ", _jsx(Text, { bold: true, color: "white", children: state.exchangeBalances?.length ?? 1 })] }), _jsxs(Text, { color: "gray", children: ["Positions: ", _jsx(Text, { bold: true, color: state.positions.length > 0 ? "yellow" : "white", children: state.positions.length })] }), _jsxs(Text, { color: "gray", children: ["Total Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", (state.exchangeBalances?.reduce((s, e) => s + parseFloat(e.equity), 0) ?? state.equity).toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["PnL: ", _jsxs(Text, { bold: true, color: state.totalPnl >= 0 ? "green" : "red", children: ["$", state.totalPnl.toFixed(2)] })] })] })) : (_jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { children: ["Price: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.price.toFixed(2)] })] }), _jsxs(Text, { color: "gray", children: ["Vol24h: ", _jsxs(Text, { color: "white", children: [state.volatility24h.toFixed(1), "%"] })] }), _jsxs(Text, { color: "gray", children: ["FR: ", _jsxs(Text, { color: state.fundingRate >= 0 ? "green" : "red", children: [(state.fundingRate * 100).toFixed(4), "%"] })] }), _jsxs(Text, { color: "gray", children: ["OI: ", _jsx(Text, { color: "white", children: state.openInterest })] })] })), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Account" }), _jsxs(Text, { children: ["Equity: ", _jsxs(Text, { bold: true, color: "white", children: ["$", state.equity.toFixed(2)] }), _jsxs(Text, { color: "gray", children: [" (peak: $", state.peakEquity.toFixed(2), ")"] })] }), _jsxs(Text, { children: ["Daily P&L: ", _jsxs(Text, { bold: true, color: pnlColor(state.dailyPnl), children: ["$", state.dailyPnl.toFixed(2)] })] })] }), _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Text, { children: ["Drawdown: ", _jsxs(Text, { bold: true, color: drawdown > 0 ? "red" : "green", children: ["$", drawdown.toFixed(2), " (", drawdownPct.toFixed(1), "%)"] })] }), _jsxs(Text, { children: ["Fills: ", _jsx(Text, { bold: true, color: "white", children: state.fills }), _jsx(Text, { color: "gray", children: " | Total P&L: " }), _jsxs(Text, { bold: true, color: pnlColor(state.totalPnl), children: ["$", state.totalPnl.toFixed(2)] })] })] })] }), state.exchangeBalances && state.exchangeBalances.length > 1 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Exchange Balances" }), _jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "gray", children: pad("Exchange", 16) }), _jsx(Text, { color: "gray", children: pad("Equity", 14) }), _jsx(Text, { color: "gray", children: "Available" })] }), state.exchangeBalances.map((eb, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: "white", children: pad(eb.exchange, 16) }), _jsx(Text, { color: "white", children: pad(`$${parseFloat(eb.equity).toFixed(2)}`, 14) }), _jsxs(Text, { color: "gray", children: ["$", parseFloat(eb.available).toFixed(2)] })] }, i)))] })), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Positions" }), state.positions.length === 0 ? (_jsx(Text, { color: "gray", children: " No open positions" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "gray", children: pad("Exchange", 14) }), _jsx(Text, { color: "gray", children: pad("Symbol", 10) }), _jsx(Text, { color: "gray", children: pad("Side", 6) }), _jsx(Text, { color: "gray", children: pad("Size", 12) }), _jsx(Text, { color: "gray", children: pad("Entry", 12) }), _jsx(Text, { color: "gray", children: pad("Mark", 12) }), _jsx(Text, { color: "gray", children: "uPnL" })] }), state.positions.map((p, i) => {
74
74
  const upnl = parseFloat(p.unrealizedPnl);
75
75
  return (_jsxs(Box, { flexDirection: "row", children: [hasMultiExchange && _jsx(Text, { color: "cyan", children: pad(p.exchange ?? "—", 14) }), _jsx(Text, { color: "white", children: pad(p.symbol, 10) }), _jsx(Text, { color: p.side.toLowerCase() === "long" ? "green" : "red", children: pad(p.side, 6) }), _jsx(Text, { color: "white", children: pad(p.size, 12) }), _jsx(Text, { color: "white", children: pad(p.entryPrice, 12) }), _jsx(Text, { color: "white", children: pad(p.markPrice, 12) }), _jsxs(Text, { color: pnlColor(upnl), children: ["$", upnl.toFixed(2)] })] }, i));
76
- })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { color: "cyan", bold: true, children: "Strategy State" }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [Object.entries(state.strategyState).slice(0, 12).map(([k, v]) => (_jsxs(Box, { marginRight: 2, children: [_jsxs(Text, { color: "gray", children: [k, ": "] }), _jsx(Text, { color: "white", children: String(v) })] }, k))), Object.keys(state.strategyState).length === 0 && (_jsx(Text, { color: "gray", children: " --" }))] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: MAX_LOG_LINES + 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Log" }), logs.length === 0 ? (_jsx(Text, { color: "gray", children: " Waiting for events..." })) : (logs.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [entry.time, " "] }), _jsx(Text, { children: colorizeLog(entry.message) })] }, i))))] }), _jsx(Box, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: _jsxs(Text, { color: "gray", children: ["[", _jsx(Text, { color: "white", bold: true, children: "Q" }), "]uit [", _jsx(Text, { color: "white", bold: true, children: "P" }), "]", paused ? "resume" : "ause"] }) })] }));
76
+ })] }))] }), _jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "green", bold: true, children: ["Bids (", bidOrders.length, ")"] }), bidOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (bidOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "green", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] }), _jsxs(Box, { flexDirection: "column", width: "50%", children: [_jsxs(Text, { color: "red", bold: true, children: ["Asks (", askOrders.length, ")"] }), askOrders.length === 0 ? (_jsx(Text, { color: "gray", children: " --" })) : (askOrders.slice(0, 8).map((o, i) => (_jsxs(Text, { color: "red", children: [pad(o.price, 12), " ", pad(o.size, 10), " ", o.type] }, i))))] })] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: state.strategyState.fundingTotal !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Funding & Arb" }), _jsxs(Box, { flexDirection: "row", marginLeft: 2, children: [_jsx(Text, { color: "gray", children: "Funding Income: " }), _jsx(Text, { color: "white", children: String(state.strategyState.fundingTotal) }), _jsx(Text, { color: "gray", children: " Positions: " }), _jsx(Text, { color: "white", children: String(state.strategyState.arbPositions ?? 0) })] }), Array.isArray(state.strategyState.arbPositionDetails) &&
77
+ state.strategyState.arbPositionDetails.map((detail, i) => (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { color: "yellow", children: detail }) }, i)))] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: "cyan", bold: true, children: "Strategy State" }), _jsxs(Box, { flexDirection: "row", flexWrap: "wrap", children: [Object.entries(state.strategyState)
78
+ .filter(([k, v]) => {
79
+ if (k === 'arbRunning' || k === 'fundingLastCheck' || k === 'fundingHistory' || k === 'arbOpenPositions')
80
+ return false;
81
+ if (Array.isArray(v) || (typeof v === 'object' && v !== null))
82
+ return false;
83
+ return true;
84
+ })
85
+ .slice(0, 12)
86
+ .map(([k, v]) => (_jsxs(Box, { marginRight: 2, children: [_jsxs(Text, { color: "gray", children: [k, ": "] }), _jsx(Text, { color: "white", children: String(v) })] }, k))), Object.keys(state.strategyState).length === 0 && (_jsx(Text, { color: "gray", children: " --" }))] })] })) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, height: MAX_LOG_LINES + 2, children: [_jsx(Text, { color: "cyan", bold: true, children: "Log" }), logs.length === 0 ? (_jsx(Text, { color: "gray", children: " Waiting for events..." })) : (logs.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { color: "gray", children: [entry.time, " "] }), _jsx(Text, { children: colorizeLog(entry.message) })] }, i))))] }), _jsx(Box, { marginTop: 1, flexDirection: "row", justifyContent: "center", children: _jsxs(Text, { color: "gray", children: ["[", _jsx(Text, { color: "white", bold: true, children: "Q" }), "]uit [", _jsx(Text, { color: "white", bold: true, children: "P" }), "]", paused ? "resume" : "ause"] }) })] }));
77
87
  }
78
88
  // ── Utility ──
79
89
  function pad(s, width) {
@@ -14,7 +14,11 @@ export declare class AsterAdapter implements ExchangeAdapter {
14
14
  private _testnet;
15
15
  private _marketsCache;
16
16
  private _marketsCacheTime;
17
+ private _accountCache;
18
+ private _positionsCache;
19
+ private _ordersCache;
17
20
  private static readonly CACHE_TTL;
21
+ private static readonly ACCOUNT_CACHE_TTL;
18
22
  constructor(apiKey?: string, apiSecret?: string, testnet?: boolean);
19
23
  get isReadOnly(): boolean;
20
24
  /** CLI symbol → Aster API symbol (ETH → ETHUSDT) */
@@ -14,7 +14,11 @@ export class AsterAdapter {
14
14
  _testnet;
15
15
  _marketsCache = null;
16
16
  _marketsCacheTime = 0;
17
- static CACHE_TTL = 10_000; // 10 seconds
17
+ _accountCache = null;
18
+ _positionsCache = null;
19
+ _ordersCache = null;
20
+ static CACHE_TTL = 30_000; // 30 seconds (markets)
21
+ static ACCOUNT_CACHE_TTL = 5_000; // 5 seconds (account/positions/orders)
18
22
  constructor(apiKey, apiSecret, testnet = false) {
19
23
  this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
20
24
  this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
@@ -137,21 +141,29 @@ export class AsterAdapter {
137
141
  }
138
142
  // ── Account ──
139
143
  async getBalance() {
144
+ if (this._accountCache && Date.now() - this._accountCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
145
+ return this._accountCache.data;
146
+ }
140
147
  const account = await this._signedGet("/fapi/v2/account");
141
148
  const totalWallet = Number(account.totalWalletBalance ?? 0);
142
149
  const unrealizedPnl = Number(account.totalUnrealizedProfit ?? 0);
143
150
  const available = Number(account.availableBalance ?? 0);
144
151
  const marginUsed = Number(account.totalInitialMargin ?? 0);
145
- return {
152
+ const result = {
146
153
  equity: String(totalWallet + unrealizedPnl),
147
154
  available: String(available),
148
155
  marginUsed: String(marginUsed),
149
156
  unrealizedPnl: String(unrealizedPnl),
150
157
  };
158
+ this._accountCache = { data: result, time: Date.now() };
159
+ return result;
151
160
  }
152
161
  async getPositions() {
162
+ if (this._positionsCache && Date.now() - this._positionsCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
163
+ return this._positionsCache.data;
164
+ }
153
165
  const data = await this._signedGet("/fapi/v2/positionRisk");
154
- return (data ?? [])
166
+ const result = (data ?? [])
155
167
  .filter((p) => Number(p.positionAmt ?? 0) !== 0)
156
168
  .map((p) => {
157
169
  const amt = Number(p.positionAmt ?? 0);
@@ -166,10 +178,15 @@ export class AsterAdapter {
166
178
  leverage: Number(p.leverage ?? 1),
167
179
  };
168
180
  });
181
+ this._positionsCache = { data: result, time: Date.now() };
182
+ return result;
169
183
  }
170
184
  async getOpenOrders() {
185
+ if (this._ordersCache && Date.now() - this._ordersCache.time < AsterAdapter.ACCOUNT_CACHE_TTL) {
186
+ return this._ordersCache.data;
187
+ }
171
188
  const orders = await this._signedGet("/fapi/v1/openOrders");
172
- return (orders ?? []).map((o) => ({
189
+ const result = (orders ?? []).map((o) => ({
173
190
  orderId: String(o.orderId ?? ""),
174
191
  symbol: this._fromApi(String(o.symbol ?? "")),
175
192
  side: String(o.side).toLowerCase(),
@@ -179,6 +196,8 @@ export class AsterAdapter {
179
196
  status: String(o.status ?? ""),
180
197
  type: String(o.type ?? ""),
181
198
  }));
199
+ this._ordersCache = { data: result, time: Date.now() };
200
+ return result;
182
201
  }
183
202
  async getOrderHistory(limit = 30) {
184
203
  // allOrders requires symbol — get from positions or recent trades
@@ -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.12",
3
+ "version": "0.7.15",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",