perp-cli 0.7.13 → 0.7.16

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.
@@ -454,6 +454,71 @@ async function fetchExchangeBalances(primaryAdapter, extraAdapters) {
454
454
  }));
455
455
  return results;
456
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
+ const total = Number(b.total);
481
+ const entryNtl = b.entryNtl ? Number(b.entryNtl) : 0;
482
+ const avgEntry = (entryNtl > 0 && total > 0) ? (entryNtl / total) : 0;
483
+ const mark = Number(markPrice);
484
+ const uPnL = avgEntry > 0 ? (mark - avgEntry) * total : 0;
485
+ hlPositions.push({
486
+ symbol: base, side: "spot", size: b.total,
487
+ entryPrice: avgEntry > 0 ? avgEntry.toPrecision(5) : "—",
488
+ markPrice,
489
+ unrealizedPnl: avgEntry > 0 ? uPnL.toFixed(2) : "—",
490
+ exchange: adapter.name,
491
+ });
492
+ }
493
+ return hlPositions;
494
+ }
495
+ if (name === "lighter") {
496
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
497
+ const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
498
+ if (!(adapter instanceof LighterAdapter))
499
+ return [];
500
+ const ltSpot = new LighterSpotAdapter(adapter);
501
+ await ltSpot.init();
502
+ const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
503
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), m.markPrice]));
504
+ const ltPositions = [];
505
+ for (const b of raw) {
506
+ if (Number(b.total) <= 0)
507
+ continue;
508
+ const base = b.token.toUpperCase();
509
+ const markPrice = priceMap.get(base) ?? "0";
510
+ if (base === "USDC" || base === "USDC_SPOT" || markPrice === "0")
511
+ continue;
512
+ ltPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
513
+ }
514
+ return ltPositions;
515
+ }
516
+ }
517
+ catch {
518
+ // Spot adapter unavailable — skip silently
519
+ }
520
+ return [];
521
+ }
457
522
  async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
458
523
  const adapters = [[primaryAdapter.name, primaryAdapter]];
459
524
  if (extraAdapters) {
@@ -462,8 +527,11 @@ async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
462
527
  }
463
528
  const all = [];
464
529
  await Promise.all(adapters.map(async ([name, a]) => {
465
- const pos = await fetchPositions(a, symbol, name);
466
- all.push(pos);
530
+ const [perpPos, spotPos] = await Promise.all([
531
+ fetchPositions(a, symbol, name),
532
+ fetchSpotPositions(a),
533
+ ]);
534
+ all.push([...perpPos, ...spotPos]);
467
535
  }));
468
536
  return all.flat();
469
537
  }
@@ -31,6 +31,71 @@ export class FundingArbStrategy {
31
31
  ctx.state.set("fundingHistory", []);
32
32
  ctx.log(` [ARB] Funding arb ready | spread >= ${params.min_spread}% | close < ${params.close_spread}% | size $${params.size_usd}`);
33
33
  ctx.log(` [ARB] Exchanges: ${params.exchanges.join(", ")}`);
34
+ // ── Recover existing positions from exchanges ──
35
+ try {
36
+ const extraAdapters = ctx.state.get("extraAdapters");
37
+ const adapters = new Map();
38
+ adapters.set(ctx.adapter.name.toLowerCase(), ctx.adapter);
39
+ if (extraAdapters) {
40
+ for (const [name, a] of extraAdapters)
41
+ adapters.set(name, a);
42
+ }
43
+ // Fetch all positions across exchanges
44
+ const positionsByExchange = new Map();
45
+ for (const [name, a] of adapters) {
46
+ try {
47
+ const positions = await a.getPositions();
48
+ positionsByExchange.set(name, positions.map(p => ({
49
+ symbol: p.symbol.toUpperCase(),
50
+ side: p.side,
51
+ size: p.size,
52
+ })));
53
+ }
54
+ catch { /* skip unavailable exchange */ }
55
+ }
56
+ // Match long/short pairs across exchanges → reconstruct arbOpenPositions
57
+ const recovered = [];
58
+ const used = new Set(); // "exchange:symbol" to avoid double-matching
59
+ for (const [exA, posA] of positionsByExchange) {
60
+ for (const pA of posA) {
61
+ const keyA = `${exA}:${pA.symbol}`;
62
+ if (used.has(keyA))
63
+ continue;
64
+ for (const [exB, posB] of positionsByExchange) {
65
+ if (exA === exB)
66
+ continue;
67
+ for (const pB of posB) {
68
+ const keyB = `${exB}:${pB.symbol}`;
69
+ if (used.has(keyB))
70
+ continue;
71
+ if (pA.symbol !== pB.symbol)
72
+ continue;
73
+ if (pA.side === pB.side)
74
+ continue;
75
+ // Found a matching pair
76
+ const longEx = pA.side === "long" ? exA : exB;
77
+ const shortEx = pA.side === "short" ? exA : exB;
78
+ const size = pA.side === "long" ? pA.size : pB.size;
79
+ recovered.push({ symbol: pA.symbol, longExchange: longEx, shortExchange: shortEx, entrySpread: 0, size });
80
+ used.add(keyA);
81
+ used.add(keyB);
82
+ break;
83
+ }
84
+ if (used.has(keyA))
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ if (recovered.length > 0) {
90
+ ctx.state.set("arbOpenPositions", recovered);
91
+ ctx.state.set("arbPositions", recovered.length);
92
+ ctx.log(` [ARB] Recovered ${recovered.length} existing position(s): ${recovered.map(p => `${p.symbol} ${p.longExchange}↔${p.shortExchange}`).join(", ")}`);
93
+ }
94
+ }
95
+ catch (err) {
96
+ const msg = err instanceof Error ? err.message : String(err);
97
+ ctx.log(` [ARB] Position recovery failed: ${msg}`);
98
+ }
34
99
  }
35
100
  async onTick(ctx, _snapshot) {
36
101
  const params = this.params;
@@ -59,7 +124,7 @@ export class FundingArbStrategy {
59
124
  }
60
125
  // ── Fetch funding income periodically (every 5 minutes) ──
61
126
  const lastCheck = ctx.state.get("fundingLastCheck");
62
- if (openPositions.length > 0 && Date.now() - lastCheck > 5 * 60 * 1000) {
127
+ if (Date.now() - lastCheck > 5 * 60 * 1000) {
63
128
  try {
64
129
  let totalFunding = 0;
65
130
  for (const [name, a] of adapters) {
@@ -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 ──
@@ -72,8 +72,18 @@ export function BotDashboard({ initialState, onQuit, onPause, subscribe }) {
72
72
  const isMultiSymbol = state.symbol.toUpperCase() === "ALL";
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
- 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"] }) })] }));
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) }), _jsx(Text, { color: isNaN(upnl) ? "gray" : pnlColor(upnl), children: isNaN(upnl) ? "$—" : `$${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))))] })] }), _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 = 30_000; // 30 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
@@ -160,6 +160,7 @@ export class HyperliquidSpotAdapter {
160
160
  total: String(b.total ?? "0"),
161
161
  available: String(Number(b.total ?? 0) - Number(b.hold ?? 0)),
162
162
  held: String(b.hold ?? "0"),
163
+ entryNtl: b.entryNtl !== undefined ? String(b.entryNtl) : undefined,
163
164
  }));
164
165
  }
165
166
  catch {
@@ -19,6 +19,7 @@ export interface SpotBalance {
19
19
  total: string;
20
20
  available: string;
21
21
  held: string;
22
+ entryNtl?: string;
22
23
  }
23
24
  export interface SpotOrderParams {
24
25
  symbol: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.13",
3
+ "version": "0.7.16",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",