perp-cli 0.7.15 → 0.7.17

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,112 @@ 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
+ // ── Same-exchange spot-perp hedges (e.g. PURR spot + PURR-PERP short on HL) ──
90
+ for (const [name, a] of adapters) {
91
+ try {
92
+ const spotState = await a._getSpotClearinghouseState?.();
93
+ if (!spotState?.balances)
94
+ continue;
95
+ const spotBalances = spotState.balances
96
+ .filter(b => Number(b.total) > 0 && b.coin.toUpperCase() !== "USDC");
97
+ const perpPositions = positionsByExchange.get(name) ?? [];
98
+ for (const perp of perpPositions) {
99
+ const perpKey = `${name}:${perp.symbol}`;
100
+ if (used.has(perpKey))
101
+ continue;
102
+ const base = perp.symbol.replace(/-PERP$/, "").toUpperCase();
103
+ const spotBal = spotBalances.find(b => b.coin.toUpperCase() === base);
104
+ if (spotBal && perp.side === "short") {
105
+ recovered.push({ symbol: base, longExchange: `${name}-spot`, shortExchange: name, entrySpread: 0, size: perp.size });
106
+ used.add(perpKey);
107
+ ctx.log(` [ARB] Detected spot-perp hedge: ${base} ${name}-spot↔${name}`);
108
+ }
109
+ }
110
+ }
111
+ catch { /* adapter doesn't support spot */ }
112
+ }
113
+ if (recovered.length > 0) {
114
+ ctx.state.set("arbOpenPositions", recovered);
115
+ ctx.state.set("arbPositions", recovered.length);
116
+ ctx.log(` [ARB] Recovered ${recovered.length} position(s): ${recovered.map(p => `${p.symbol} ${p.longExchange}↔${p.shortExchange}`).join(", ")}`);
117
+ }
118
+ // ── Load historical funding income ──
119
+ let historicalFunding = 0;
120
+ for (const [name, a] of adapters) {
121
+ try {
122
+ const payments = await a.getFundingPayments(200);
123
+ const sum = payments.reduce((s, p) => s + parseFloat(p.payment), 0);
124
+ if (Math.abs(sum) > 0.001) {
125
+ historicalFunding += sum;
126
+ ctx.log(` [FUND] ${name}: $${sum.toFixed(4)} historical funding`);
127
+ }
128
+ }
129
+ catch { /* not all adapters support this */ }
130
+ }
131
+ if (Math.abs(historicalFunding) > 0.001) {
132
+ ctx.state.set("fundingIncome", historicalFunding);
133
+ ctx.log(` [FUND] Total historical funding: $${historicalFunding.toFixed(4)}`);
134
+ }
135
+ }
136
+ catch (err) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ ctx.log(` [ARB] Position recovery failed: ${msg}`);
139
+ }
34
140
  }
35
141
  async onTick(ctx, _snapshot) {
36
142
  const params = this.params;
@@ -59,15 +165,15 @@ export class FundingArbStrategy {
59
165
  }
60
166
  // ── Fetch funding income periodically (every 5 minutes) ──
61
167
  const lastCheck = ctx.state.get("fundingLastCheck");
62
- if (openPositions.length > 0 && Date.now() - lastCheck > 5 * 60 * 1000) {
168
+ if (Date.now() - lastCheck > 5 * 60 * 1000) {
63
169
  try {
64
170
  let totalFunding = 0;
65
171
  for (const [name, a] of adapters) {
66
172
  try {
67
173
  const payments = await a.getFundingPayments(50);
68
174
  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);
175
+ const recent = payments.filter(p => p.time > startTime - 5 * 60 * 1000);
176
+ const sum = recent.reduce((s, p) => s + parseFloat(p.payment), 0);
71
177
  totalFunding += sum;
72
178
  if (Math.abs(sum) > 0.001) {
73
179
  ctx.log(` [FUND] ${name}: $${sum.toFixed(4)} funding received`);
@@ -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.17",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",