perp-cli 0.7.13 → 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.
@@ -454,6 +454,60 @@ 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
+ 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
+ }
457
511
  async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
458
512
  const adapters = [[primaryAdapter.name, primaryAdapter]];
459
513
  if (extraAdapters) {
@@ -462,8 +516,11 @@ async function fetchAllPositions(primaryAdapter, symbol, extraAdapters) {
462
516
  }
463
517
  const all = [];
464
518
  await Promise.all(adapters.map(async ([name, a]) => {
465
- const pos = await fetchPositions(a, symbol, name);
466
- all.push(pos);
519
+ const [perpPos, spotPos] = await Promise.all([
520
+ fetchPositions(a, symbol, name),
521
+ fetchSpotPositions(a),
522
+ ]);
523
+ all.push([...perpPos, ...spotPos]);
467
524
  }));
468
525
  return all.flat();
469
526
  }
@@ -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 = 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.7.13",
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",