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.
package/dist/bot/engine.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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) }),
|
|
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 {
|