perp-cli 0.7.12 → 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) {
@@ -14,7 +14,7 @@ export class AsterAdapter {
14
14
  _testnet;
15
15
  _marketsCache = null;
16
16
  _marketsCacheTime = 0;
17
- static CACHE_TTL = 10_000; // 10 seconds
17
+ static CACHE_TTL = 30_000; // 30 seconds
18
18
  constructor(apiKey, apiSecret, testnet = false) {
19
19
  this._apiKey = apiKey || process.env.ASTER_API_KEY || "";
20
20
  this._apiSecret = apiSecret || process.env.ASTER_API_SECRET || "";
@@ -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.13",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",