perp-cli 0.7.15 → 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.
@@ -477,7 +477,18 @@ async function fetchSpotPositions(adapter) {
477
477
  const markPrice = priceMap.get(base) ?? "0";
478
478
  if (base === "USDC" || markPrice === "0")
479
479
  continue;
480
- hlPositions.push({ symbol: base, side: "spot", size: b.total, entryPrice: "—", markPrice, unrealizedPnl: "—", exchange: adapter.name });
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
+ });
481
492
  }
482
493
  return hlPositions;
483
494
  }
@@ -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) {
@@ -72,7 +72,7 @@ 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));
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
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
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
78
  .filter(([k, v]) => {
@@ -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.15",
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",