perp-cli 0.4.0 → 0.4.8
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/README.md +2 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +12 -9
- package/dist/commands/arb/index.js +18 -3
- package/dist/commands/arb-auto.js +885 -67
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +118 -43
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +181 -92
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +132 -37
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +148 -99
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.d.ts +44 -0
- package/dist/smart-order.js +145 -0
- package/dist/trade-validator.js +2 -1
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +102 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified market data feeds for CLI streaming commands.
|
|
3
|
+
*
|
|
4
|
+
* - Hyperliquid: native WebSocket (allMids, l2Book, trades, candle)
|
|
5
|
+
* - Lighter: REST polling (no public market data WS)
|
|
6
|
+
* - Pacifica: handled via existing PacificaWSClient in stream.ts
|
|
7
|
+
*/
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
// ── Hyperliquid Market Feed (native WebSocket) ──
|
|
10
|
+
export class HyperliquidMarketFeed extends EventEmitter {
|
|
11
|
+
ws = null;
|
|
12
|
+
closed = false;
|
|
13
|
+
reconnectTimer = null;
|
|
14
|
+
subs = [];
|
|
15
|
+
get wsUrl() { return "wss://api.hyperliquid.xyz/ws"; }
|
|
16
|
+
async connect() {
|
|
17
|
+
const { WebSocket } = await import("ws");
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
20
|
+
this.ws.on("open", () => {
|
|
21
|
+
// Re-subscribe on reconnect
|
|
22
|
+
for (const sub of this.subs)
|
|
23
|
+
this.send({ method: "subscribe", subscription: sub });
|
|
24
|
+
this.emit("connected");
|
|
25
|
+
resolve();
|
|
26
|
+
});
|
|
27
|
+
this.ws.on("error", (err) => {
|
|
28
|
+
if (!this.closed)
|
|
29
|
+
reject(err);
|
|
30
|
+
});
|
|
31
|
+
this.ws.on("close", () => {
|
|
32
|
+
if (!this.closed) {
|
|
33
|
+
this.emit("disconnected");
|
|
34
|
+
this.scheduleReconnect();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
this.ws.on("message", (raw) => {
|
|
38
|
+
try {
|
|
39
|
+
this.handleMessage(JSON.parse(String(raw)));
|
|
40
|
+
}
|
|
41
|
+
catch { /* ignore */ }
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
send(data) {
|
|
46
|
+
if (this.ws?.readyState === 1)
|
|
47
|
+
this.ws.send(JSON.stringify(data));
|
|
48
|
+
}
|
|
49
|
+
sub(subscription) {
|
|
50
|
+
this.subs.push(subscription);
|
|
51
|
+
this.send({ method: "subscribe", subscription });
|
|
52
|
+
}
|
|
53
|
+
scheduleReconnect() {
|
|
54
|
+
if (this.closed)
|
|
55
|
+
return;
|
|
56
|
+
this.reconnectTimer = setTimeout(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await this.connect();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
this.scheduleReconnect();
|
|
62
|
+
}
|
|
63
|
+
}, 3000);
|
|
64
|
+
}
|
|
65
|
+
subscribePrices() { this.sub({ type: "allMids" }); }
|
|
66
|
+
subscribeBook(symbol) { this.sub({ type: "l2Book", coin: symbol.toUpperCase() }); }
|
|
67
|
+
subscribeTrades(symbol) { this.sub({ type: "trades", coin: symbol.toUpperCase() }); }
|
|
68
|
+
subscribeCandle(symbol, interval) {
|
|
69
|
+
this.sub({ type: "candle", coin: symbol.toUpperCase(), interval });
|
|
70
|
+
}
|
|
71
|
+
handleMessage(msg) {
|
|
72
|
+
const channel = String(msg.channel ?? "");
|
|
73
|
+
const data = msg.data;
|
|
74
|
+
if (!data)
|
|
75
|
+
return;
|
|
76
|
+
if (channel === "allMids") {
|
|
77
|
+
const mids = (data.mids ?? data);
|
|
78
|
+
const updates = Object.entries(mids).map(([symbol, mid]) => ({
|
|
79
|
+
symbol, mid: Number(mid),
|
|
80
|
+
}));
|
|
81
|
+
this.emit("prices", updates);
|
|
82
|
+
}
|
|
83
|
+
else if (channel === "l2Book") {
|
|
84
|
+
const d = data;
|
|
85
|
+
const levels = d.levels;
|
|
86
|
+
if (levels) {
|
|
87
|
+
this.emit("book", {
|
|
88
|
+
bids: levels[0].map((l) => [l.px, l.sz]),
|
|
89
|
+
asks: levels[1].map((l) => [l.px, l.sz]),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (channel === "trades") {
|
|
94
|
+
const trades = Array.isArray(data) ? data : [];
|
|
95
|
+
for (const t of trades) {
|
|
96
|
+
// HL side: "A" = sell (ask), "B" = buy (bid)
|
|
97
|
+
const rawSide = String(t.side ?? "");
|
|
98
|
+
this.emit("trade", {
|
|
99
|
+
side: rawSide === "A" ? "sell" : rawSide === "B" ? "buy" : rawSide,
|
|
100
|
+
price: String(t.px ?? ""),
|
|
101
|
+
size: String(t.sz ?? ""),
|
|
102
|
+
time: Number(t.time ?? Date.now()),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (channel === "candle") {
|
|
107
|
+
const d = data;
|
|
108
|
+
this.emit("candle", {
|
|
109
|
+
o: String(d.o ?? ""), h: String(d.h ?? ""), l: String(d.l ?? ""),
|
|
110
|
+
c: String(d.c ?? ""), v: String(d.v ?? ""), t: Number(d.t ?? 0),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
close() {
|
|
115
|
+
this.closed = true;
|
|
116
|
+
if (this.reconnectTimer)
|
|
117
|
+
clearTimeout(this.reconnectTimer);
|
|
118
|
+
if (this.ws) {
|
|
119
|
+
this.ws.close();
|
|
120
|
+
this.ws = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── Lighter Market Feed (REST polling) ──
|
|
125
|
+
const LIGHTER_BASE = "https://mainnet.zklighter.elliot.ai/api/v1";
|
|
126
|
+
export class LighterMarketFeed extends EventEmitter {
|
|
127
|
+
closed = false;
|
|
128
|
+
timers = [];
|
|
129
|
+
marketMap = new Map();
|
|
130
|
+
pricesMs;
|
|
131
|
+
bookMs;
|
|
132
|
+
candleMs;
|
|
133
|
+
constructor(opts) {
|
|
134
|
+
super();
|
|
135
|
+
this.pricesMs = opts?.pricesIntervalMs ?? 3000;
|
|
136
|
+
this.bookMs = opts?.bookIntervalMs ?? 2000;
|
|
137
|
+
this.candleMs = opts?.candleIntervalMs ?? 5000;
|
|
138
|
+
}
|
|
139
|
+
async connect() {
|
|
140
|
+
const res = await fetch(`${LIGHTER_BASE}/orderBookDetails`);
|
|
141
|
+
const json = await res.json();
|
|
142
|
+
for (const m of json.order_book_details ?? []) {
|
|
143
|
+
if (m.market_type === "perp")
|
|
144
|
+
this.marketMap.set(m.symbol.toUpperCase(), m.market_id);
|
|
145
|
+
}
|
|
146
|
+
this.emit("connected");
|
|
147
|
+
}
|
|
148
|
+
getMarketId(symbol) {
|
|
149
|
+
const id = this.marketMap.get(symbol.toUpperCase());
|
|
150
|
+
if (id === undefined)
|
|
151
|
+
throw new Error(`Unknown Lighter market: ${symbol}`);
|
|
152
|
+
return id;
|
|
153
|
+
}
|
|
154
|
+
poll(fn, intervalMs) {
|
|
155
|
+
fn().catch(() => { });
|
|
156
|
+
this.timers.push(setInterval(() => { if (!this.closed)
|
|
157
|
+
fn().catch(() => { }); }, intervalMs));
|
|
158
|
+
}
|
|
159
|
+
subscribePrices() {
|
|
160
|
+
this.poll(async () => {
|
|
161
|
+
const res = await fetch(`${LIGHTER_BASE}/orderBookDetails`);
|
|
162
|
+
const json = await res.json();
|
|
163
|
+
const updates = (json.order_book_details ?? [])
|
|
164
|
+
.filter(m => m.market_type === "perp")
|
|
165
|
+
.map(m => ({ symbol: m.symbol, mid: m.last_trade_price }));
|
|
166
|
+
this.emit("prices", updates);
|
|
167
|
+
}, this.pricesMs);
|
|
168
|
+
}
|
|
169
|
+
subscribeBook(symbol) {
|
|
170
|
+
const marketId = this.getMarketId(symbol);
|
|
171
|
+
this.poll(async () => {
|
|
172
|
+
const res = await fetch(`${LIGHTER_BASE}/orderBookOrders?market_id=${marketId}&limit=20`);
|
|
173
|
+
const json = await res.json();
|
|
174
|
+
this.emit("book", {
|
|
175
|
+
bids: (json.bids ?? []).map(b => [b.price, b.remaining_base_amount]),
|
|
176
|
+
asks: (json.asks ?? []).map(a => [a.price, a.remaining_base_amount]),
|
|
177
|
+
});
|
|
178
|
+
}, this.bookMs);
|
|
179
|
+
}
|
|
180
|
+
subscribeTrades(symbol) {
|
|
181
|
+
const marketId = this.getMarketId(symbol);
|
|
182
|
+
let lastTid = 0;
|
|
183
|
+
this.poll(async () => {
|
|
184
|
+
const res = await fetch(`${LIGHTER_BASE}/recentTrades?market_id=${marketId}&limit=20`);
|
|
185
|
+
const json = await res.json();
|
|
186
|
+
for (const t of json.trades ?? []) {
|
|
187
|
+
if (t.timestamp > lastTid) {
|
|
188
|
+
lastTid = t.timestamp;
|
|
189
|
+
this.emit("trade", {
|
|
190
|
+
side: t.is_maker_ask ? "buy" : "sell",
|
|
191
|
+
price: t.price,
|
|
192
|
+
size: t.size,
|
|
193
|
+
time: t.timestamp,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}, this.bookMs);
|
|
198
|
+
}
|
|
199
|
+
subscribeCandle(symbol, interval) {
|
|
200
|
+
const marketId = this.getMarketId(symbol);
|
|
201
|
+
this.poll(async () => {
|
|
202
|
+
const now = Math.floor(Date.now() / 1000);
|
|
203
|
+
const res = await fetch(`${LIGHTER_BASE}/candles?market_id=${marketId}&resolution=${interval}&count_back=1&end_timestamp=${now}`);
|
|
204
|
+
const json = await res.json();
|
|
205
|
+
const c = json.candles?.[0];
|
|
206
|
+
if (c) {
|
|
207
|
+
this.emit("candle", {
|
|
208
|
+
o: c.open, h: c.high, l: c.low, c: c.close, v: c.volume, t: c.timestamp,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}, this.candleMs);
|
|
212
|
+
}
|
|
213
|
+
close() {
|
|
214
|
+
this.closed = true;
|
|
215
|
+
for (const t of this.timers)
|
|
216
|
+
clearInterval(t);
|
|
217
|
+
this.timers = [];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// ── Factory ──
|
|
221
|
+
export function createMarketFeed(exchange) {
|
|
222
|
+
if (exchange === "hyperliquid")
|
|
223
|
+
return new HyperliquidMarketFeed();
|
|
224
|
+
if (exchange === "lighter")
|
|
225
|
+
return new LighterMarketFeed();
|
|
226
|
+
throw new Error(`Use PacificaWSClient for pacifica, not createMarketFeed`);
|
|
227
|
+
}
|
package/package.json
CHANGED
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: perp-cli
|
|
3
|
-
description: "Multi-DEX perpetual futures trading CLI for Pacifica (Solana), Hyperliquid (EVM), and Lighter (Ethereum). Use when user asks to trade perps, check funding rates, bridge USDC, manage positions,
|
|
4
|
-
allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
|
|
3
|
+
description: "Multi-DEX perpetual futures trading CLI for Pacifica (Solana), Hyperliquid (EVM), and Lighter (Ethereum). Use when user asks to: trade perps, check funding rates, scan arbitrage (perp-perp or spot-perp), delta-neutral strategies, bridge USDC, manage positions/orders, deposit/withdraw, spot+perp hedge, or mentions perp-cli, hypurrquant, Pacifica, Hyperliquid, Lighter, HyperEVM, funding arb, U-token (UBTC/UETH/USOL)."
|
|
4
|
+
allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*), Bash(bash scripts/*.sh:*)"
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
7
|
author: hypurrquant
|
|
8
|
-
version: "0.4.
|
|
8
|
+
version: "0.4.7"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# perp-cli Agent Guide
|
|
@@ -59,7 +59,9 @@ perp --json arb exec <SYM> <longEx> <shortEx> <$> --leverage <N> --isolated # e
|
|
|
59
59
|
|
|
60
60
|
# Single exchange trading (when not doing arb)
|
|
61
61
|
perp --json -e <EX> trade market <SYM> buy <SIZE>
|
|
62
|
+
perp --json -e <EX> trade market <SYM> buy <SIZE> --smart # IOC limit at best ask + 1 tick (less slippage)
|
|
62
63
|
perp --json -e <EX> trade close <SYM>
|
|
64
|
+
perp --json -e <EX> trade close <SYM> --smart # smart close at best bid/ask
|
|
63
65
|
|
|
64
66
|
# Risk (only if you need to set limits — portfolio already includes risk)
|
|
65
67
|
perp --json risk limits --max-leverage 5
|
|
@@ -87,11 +89,18 @@ perp --json --ndjson -e <EX> market list
|
|
|
87
89
|
|
|
88
90
|
# Prevent duplicate orders with client ID
|
|
89
91
|
perp --json -e <EX> trade market <SYM> buy <SIZE> --client-id <UNIQUE_ID>
|
|
92
|
+
|
|
93
|
+
# Smart order: IOC limit at best bid/ask + 1 tick instead of raw market order
|
|
94
|
+
# Fills only at top-of-book price — no multi-level sweep slippage
|
|
95
|
+
perp --json -e <EX> trade market <SYM> buy <SIZE> --smart
|
|
96
|
+
perp --json -e <EX> trade close <SYM> --smart
|
|
97
|
+
perp --json arb exec <SYM> <longEx> <shortEx> <$> --smart # smart arb entry
|
|
98
|
+
perp --json arb close <SYM> --smart # smart arb close
|
|
90
99
|
```
|
|
91
100
|
|
|
92
101
|
All string outputs are auto-sanitized (control chars stripped, prompt injection patterns blocked).
|
|
93
102
|
|
|
94
|
-
## Arb Workflow
|
|
103
|
+
## Arb Workflow (Perp-Perp)
|
|
95
104
|
|
|
96
105
|
```
|
|
97
106
|
1. perp --json portfolio → check balances across all exchanges
|
|
@@ -110,6 +119,39 @@ All string outputs are auto-sanitized (control chars stripped, prompt injection
|
|
|
110
119
|
→ --pair <longEx>:<shortEx>: specify which pair if multiple
|
|
111
120
|
```
|
|
112
121
|
|
|
122
|
+
## Spot+Perp Arb Workflow
|
|
123
|
+
|
|
124
|
+
Spot has 0 funding cost → spread = |perp funding rate|. Only needs 1 exchange with spot + any exchange with perp.
|
|
125
|
+
|
|
126
|
+
**Spot exchanges:** HL (Hyperliquid), LT (Lighter). Pacifica is perp-only.
|
|
127
|
+
**U-token mapping:** HL spot uses Unit protocol bridged tokens — UBTC=BTC, UETH=ETH, USOL=SOL, UFART=FARTCOIN. Resolved automatically.
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
1. perp --json arb scan --mode spot-perp → find spot+perp opportunities
|
|
131
|
+
perp --json arb scan --mode all → perp-perp + spot-perp combined
|
|
132
|
+
2. [Show opportunity to user, get confirmation]
|
|
133
|
+
3. perp --json arb spot-exec <SYM> <spotEx> <perpEx> <$>
|
|
134
|
+
Examples:
|
|
135
|
+
perp --json arb spot-exec ETH hl hl 100 # ETH spot(HL/UETH) + perp(HL)
|
|
136
|
+
perp --json arb spot-exec BTC hl lt 50 # BTC spot(HL/UBTC) + perp(LT)
|
|
137
|
+
perp --json arb spot-exec LINK lt hl 100 # LINK spot(LT) + perp(HL)
|
|
138
|
+
→ cross-validates spot vs perp price (>5% deviation = abort, wrong token)
|
|
139
|
+
→ sequential execution if same exchange (nonce collision avoidance)
|
|
140
|
+
→ parallel execution if different exchanges
|
|
141
|
+
→ auto-rollback if one leg fails
|
|
142
|
+
Options:
|
|
143
|
+
--leverage <N> set perp leverage before entry
|
|
144
|
+
--isolated use isolated margin on perp side
|
|
145
|
+
4. perp --json arb status → shows spot-perp positions (spot leg labeled)
|
|
146
|
+
5. perp --json arb spot-close <SYM> --spot-exch <hl|lt>
|
|
147
|
+
→ sells spot + buys back perp simultaneously
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Available spot+perp pairs (13):**
|
|
151
|
+
- HL spot: BTC (UBTC), ETH (UETH), SOL (USOL), FARTCOIN (UFART), HYPE, AZTEC, STABLE
|
|
152
|
+
- LT spot: ETH, AZTEC, AAVE, LINK, UNI, LIT, LDO, SKY
|
|
153
|
+
- All have perps on HL and LT. Pacifica perps available when API is up.
|
|
154
|
+
|
|
113
155
|
## Position Sizing
|
|
114
156
|
|
|
115
157
|
- Single position < **80%** of that exchange's available balance
|
|
@@ -167,6 +209,39 @@ If `error.retryable` is `false`, do NOT retry — fix the cause first.
|
|
|
167
209
|
| `DUPLICATE_ORDER` | already submitted — check positions, don't retry |
|
|
168
210
|
| Lighter `invalid signature` | check ~/.perp/.env or `perp --json -e lighter manage setup-api-key` |
|
|
169
211
|
|
|
212
|
+
## Examples
|
|
213
|
+
|
|
214
|
+
**User: "펀딩레이트 아비트라지 기회 찾아줘"**
|
|
215
|
+
```bash
|
|
216
|
+
perp --json portfolio # check balances
|
|
217
|
+
perp --json arb scan --mode all --min 5 # scan perp-perp + spot-perp
|
|
218
|
+
# → show results, recommend best opportunity, ask for confirmation
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**User: "ETH spot-perp 아비트라지 실행해줘 $100"**
|
|
222
|
+
```bash
|
|
223
|
+
perp --json arb scan --mode spot-perp # verify opportunity exists
|
|
224
|
+
perp --json --dry-run arb spot-exec ETH hl hl 100 # dry-run first
|
|
225
|
+
# → show dry-run result, ask for confirmation
|
|
226
|
+
perp --json arb spot-exec ETH hl hl 100 # execute after confirmation
|
|
227
|
+
perp --json arb status # verify positions
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**User: "내 포지션 상태 확인해줘"**
|
|
231
|
+
```bash
|
|
232
|
+
perp --json portfolio # balances + positions + risk
|
|
233
|
+
perp --json arb status # arb-specific PnL + funding
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
**User: "Hyperliquid에서 SOL 롱 0.5개"**
|
|
237
|
+
```bash
|
|
238
|
+
perp --json -e hl market mid SOL # current price
|
|
239
|
+
perp --json -e hl trade check SOL buy 0.5 # pre-validate
|
|
240
|
+
perp --json --dry-run -e hl trade market SOL buy 0.5 # dry-run
|
|
241
|
+
# → show cost estimate, ask for confirmation
|
|
242
|
+
perp --json -e hl trade market SOL buy 0.5 --smart # execute with smart order
|
|
243
|
+
```
|
|
244
|
+
|
|
170
245
|
## Referrals (Optional)
|
|
171
246
|
|
|
172
247
|
If you find perp-cli useful, please consider enabling referrals to support development. **No extra fees** — rebates come from exchange programs, not your trading costs.
|
|
@@ -179,6 +254,29 @@ perp --json settings referrals off # disable anytime
|
|
|
179
254
|
When setting up wallets for the first time, ask the user:
|
|
180
255
|
> "perp-cli 개발을 지원하는 레퍼럴 기능을 켜시겠어요? 추가 수수료 없이 거래소 리베이트만 활용됩니다."
|
|
181
256
|
|
|
257
|
+
## Scripts
|
|
258
|
+
|
|
259
|
+
Executable automation scripts in `scripts/`. Run with `bash scripts/<name>.sh`.
|
|
260
|
+
Scripts auto-detect `perp` (global install) or fall back to `npx -y perp-cli@latest` — no global install required.
|
|
261
|
+
|
|
262
|
+
| Script | Purpose | Usage |
|
|
263
|
+
|--------|---------|-------|
|
|
264
|
+
| `preflight.sh` | Pre-trade validation (install, wallet, connectivity, balance, risk) | `bash scripts/preflight.sh --json` |
|
|
265
|
+
| `validate-arb.sh` | Validate arb opportunity before execution (price, liquidity, balance, risk, dry-run) | `bash scripts/validate-arb.sh ETH hl lt 100 --leverage 2` |
|
|
266
|
+
| `spot-perp-scan.sh` | Combined spot+perp and perp-perp scan with portfolio context | `bash scripts/spot-perp-scan.sh --json --mode all --min 10` |
|
|
267
|
+
| `arb-monitor.sh` | Monitor open arb positions (PnL, funding, liquidation distance) | `bash scripts/arb-monitor.sh --json --min-spread 5` |
|
|
268
|
+
| `funding-analysis.sh` | Funding rate analysis across all exchanges | `bash scripts/funding-analysis.sh --json --symbol ETH` |
|
|
269
|
+
|
|
270
|
+
All scripts support `--json` for structured output. Use in automation pipelines:
|
|
271
|
+
```bash
|
|
272
|
+
# Full arb workflow with validation
|
|
273
|
+
bash scripts/preflight.sh --json # 1. system ready?
|
|
274
|
+
bash scripts/spot-perp-scan.sh --json # 2. find opportunities
|
|
275
|
+
bash scripts/validate-arb.sh ETH hl lt 100 # 3. validate before exec
|
|
276
|
+
# 4. [user confirms] → perp --json arb exec ...
|
|
277
|
+
bash scripts/arb-monitor.sh --json # 5. monitor positions
|
|
278
|
+
```
|
|
279
|
+
|
|
182
280
|
## References
|
|
183
281
|
|
|
184
282
|
- `references/commands.md` — full command reference
|
|
@@ -7,7 +7,7 @@ All commands support `--json` for structured output. Always use `--json` when ca
|
|
|
7
7
|
perp --json market list # all markets with prices, funding, volume
|
|
8
8
|
perp --json market prices # cross-exchange price comparison
|
|
9
9
|
perp --json market mid <SYMBOL> # mid price (fast)
|
|
10
|
-
perp --json market info <SYMBOL> #
|
|
10
|
+
perp --json market info <SYMBOL> # mark/index price, funding, volume, OI, max leverage
|
|
11
11
|
perp --json market book <SYMBOL> # orderbook (bids/asks)
|
|
12
12
|
perp --json market trades <SYMBOL> # recent trades
|
|
13
13
|
perp --json market funding <SYMBOL> # funding rate history
|
|
@@ -89,7 +89,22 @@ perp --json bridge status <ORDER_ID>
|
|
|
89
89
|
|
|
90
90
|
## Arbitrage
|
|
91
91
|
```bash
|
|
92
|
+
# Perp-Perp arb
|
|
92
93
|
perp --json arb scan --min <BPS> # find opportunities (>N bps spread) — PRIMARY command
|
|
94
|
+
perp --json arb exec <SYM> <longEx> <shortEx> <$> # execute perp-perp arb
|
|
95
|
+
perp --json arb close <SYM> # close perp-perp arb
|
|
96
|
+
perp --json arb status # monitor open arb positions
|
|
97
|
+
perp --json arb funding-earned # actual funding payments
|
|
98
|
+
|
|
99
|
+
# Spot+Perp arb (spot funding = 0, spread = |perp funding|)
|
|
100
|
+
perp --json arb scan --mode spot-perp # scan spot+perp opportunities
|
|
101
|
+
perp --json arb scan --mode all # both perp-perp + spot-perp
|
|
102
|
+
perp --json arb spot-exec <SYM> <spotEx> <perpEx> <$> # execute spot+perp arb
|
|
103
|
+
perp --json arb spot-close <SYM> --spot-exch <hl|lt> # close spot+perp arb
|
|
104
|
+
# spotEx: hl (Hyperliquid) or lt (Lighter). perpEx: hl, lt, pac.
|
|
105
|
+
# HL spot uses U-tokens: BTC→UBTC, ETH→UETH, SOL→USOL (auto-resolved)
|
|
106
|
+
|
|
107
|
+
# Other arb tools
|
|
93
108
|
perp --json arb funding # detailed funding analysis
|
|
94
109
|
perp --json arb dex # HIP-3 cross-dex arb (Hyperliquid)
|
|
95
110
|
perp --json gap show # cross-exchange price gaps
|
|
@@ -173,7 +173,7 @@ Desired size: 1.0
|
|
|
173
173
|
|
|
174
174
|
**CRITICAL: Each exchange has its own minimum order size and step size.**
|
|
175
175
|
```bash
|
|
176
|
-
perp --json -e <EX> market info <SYM> # check
|
|
176
|
+
perp --json -e <EX> market info <SYM> # check maxLeverage, volume, openInterest
|
|
177
177
|
```
|
|
178
178
|
Round `ORDER_SIZE` to the coarser step size of the two exchanges. If the matched size falls below either exchange's minimum, the arb is NOT executable at this size — reduce target or skip.
|
|
179
179
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Monitor open arb positions and evaluate exit conditions
|
|
3
|
+
# Usage: ./arb-monitor.sh [--json] [--min-spread 5]
|
|
4
|
+
# Checks: current spread vs entry, funding earned, PnL, liquidation distance
|
|
5
|
+
# Returns JSON with position health + actionable recommendations
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Auto-detect perp command (supports npx fallback for agents without install permissions)
|
|
10
|
+
if command -v perp &>/dev/null; then
|
|
11
|
+
PERP="perp"
|
|
12
|
+
else
|
|
13
|
+
PERP="npx -y perp-cli@latest"
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
JSON_MODE=false
|
|
17
|
+
MIN_SPREAD=5
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--json) JSON_MODE=true; shift ;;
|
|
21
|
+
--min-spread) MIN_SPREAD="$2"; shift 2 ;;
|
|
22
|
+
*) shift ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
# 1. Get current arb status
|
|
27
|
+
ARB_STATUS=$($PERP --json arb status 2>/dev/null || echo '{"ok":false}')
|
|
28
|
+
if ! echo "$ARB_STATUS" | grep -q '"ok":true'; then
|
|
29
|
+
if $JSON_MODE; then
|
|
30
|
+
echo '{"ok":false,"error":"No arb positions or arb status unavailable"}'
|
|
31
|
+
else
|
|
32
|
+
echo "No open arb positions found."
|
|
33
|
+
fi
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# 2. Get current spreads
|
|
38
|
+
ARB_SCAN=$($PERP --json arb scan --min 0 2>/dev/null || echo '{"ok":false,"data":{"opportunities":[]}}')
|
|
39
|
+
|
|
40
|
+
# 3. Get funding earned
|
|
41
|
+
FUNDING=$($PERP --json arb funding-earned 2>/dev/null || echo '{"ok":false}')
|
|
42
|
+
|
|
43
|
+
# 4. Get liquidation distances
|
|
44
|
+
LIQ_DIST=$($PERP --json risk liquidation-distance 2>/dev/null || echo '{"ok":false}')
|
|
45
|
+
|
|
46
|
+
# 5. Output combined analysis
|
|
47
|
+
if $JSON_MODE; then
|
|
48
|
+
cat <<EOF
|
|
49
|
+
{
|
|
50
|
+
"ok": true,
|
|
51
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
52
|
+
"positions": $ARB_STATUS,
|
|
53
|
+
"currentSpreads": $ARB_SCAN,
|
|
54
|
+
"fundingEarned": $FUNDING,
|
|
55
|
+
"liquidationDistance": $LIQ_DIST,
|
|
56
|
+
"minSpreadThreshold": $MIN_SPREAD
|
|
57
|
+
}
|
|
58
|
+
EOF
|
|
59
|
+
else
|
|
60
|
+
echo "=== Arb Position Monitor ==="
|
|
61
|
+
echo "Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
62
|
+
echo ""
|
|
63
|
+
echo "--- Positions ---"
|
|
64
|
+
echo "$ARB_STATUS" | jq -r '.data // empty'
|
|
65
|
+
echo ""
|
|
66
|
+
echo "--- Funding Earned ---"
|
|
67
|
+
echo "$FUNDING" | jq -r '.data // empty'
|
|
68
|
+
echo ""
|
|
69
|
+
echo "--- Liquidation Distance ---"
|
|
70
|
+
echo "$LIQ_DIST" | jq -r '.data // empty'
|
|
71
|
+
echo ""
|
|
72
|
+
echo "Min spread threshold: ${MIN_SPREAD} bps"
|
|
73
|
+
echo "Recommendation: Check if current spreads exceed ${MIN_SPREAD} bps to continue holding."
|
|
74
|
+
fi
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Funding rate analysis across all exchanges
|
|
3
|
+
# Usage: ./funding-analysis.sh [--json] [--symbol ETH] [--top 10]
|
|
4
|
+
# Fetches current + historical funding data for comparison
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Auto-detect perp command (supports npx fallback for agents without install permissions)
|
|
9
|
+
if command -v perp &>/dev/null; then
|
|
10
|
+
PERP="perp"
|
|
11
|
+
else
|
|
12
|
+
PERP="npx -y perp-cli@latest"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
JSON_MODE=false
|
|
16
|
+
SYMBOL=""
|
|
17
|
+
TOP=10
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--json) JSON_MODE=true; shift ;;
|
|
21
|
+
--symbol) SYMBOL="$2"; shift 2 ;;
|
|
22
|
+
--top) TOP="$2"; shift 2 ;;
|
|
23
|
+
*) shift ;;
|
|
24
|
+
esac
|
|
25
|
+
done
|
|
26
|
+
|
|
27
|
+
if [[ -n "$SYMBOL" ]]; then
|
|
28
|
+
# Single symbol detailed analysis
|
|
29
|
+
FUNDING=$($PERP --json market funding "$SYMBOL" 2>/dev/null || echo '{"ok":false}')
|
|
30
|
+
SCAN=$($PERP --json arb scan --min 0 2>/dev/null || echo '{"ok":false}')
|
|
31
|
+
SPOT_SCAN=$($PERP --json arb scan --mode spot-perp --min 0 2>/dev/null || echo '{"ok":false}')
|
|
32
|
+
|
|
33
|
+
if $JSON_MODE; then
|
|
34
|
+
cat <<EOF
|
|
35
|
+
{
|
|
36
|
+
"ok": true,
|
|
37
|
+
"symbol": "$SYMBOL",
|
|
38
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
39
|
+
"funding": $FUNDING,
|
|
40
|
+
"perpPerpArb": $SCAN,
|
|
41
|
+
"spotPerpArb": $SPOT_SCAN
|
|
42
|
+
}
|
|
43
|
+
EOF
|
|
44
|
+
else
|
|
45
|
+
echo "=== Funding Analysis: $SYMBOL ==="
|
|
46
|
+
echo ""
|
|
47
|
+
echo "--- Current Funding Rates ---"
|
|
48
|
+
echo "$FUNDING" | jq -r '.data // empty' 2>/dev/null || echo "(unavailable)"
|
|
49
|
+
echo ""
|
|
50
|
+
echo "--- Perp-Perp Arb ---"
|
|
51
|
+
echo "$SCAN" | jq -r '
|
|
52
|
+
.data // [] | map(select(.symbol == "'"$SYMBOL"'")) | .[] |
|
|
53
|
+
" \(.longExch // .longExchange)→\(.shortExch // .shortExchange): \(.netSpread // .spread)bps"
|
|
54
|
+
' 2>/dev/null || echo " (no opportunity)"
|
|
55
|
+
echo ""
|
|
56
|
+
echo "--- Spot+Perp Arb ---"
|
|
57
|
+
echo "$SPOT_SCAN" | jq -r '
|
|
58
|
+
.data // [] | map(select(.symbol == "'"$SYMBOL"'")) | .[] |
|
|
59
|
+
" \(.direction): \(.annualSpreadPct // .spread)% annual"
|
|
60
|
+
' 2>/dev/null || echo " (no opportunity)"
|
|
61
|
+
fi
|
|
62
|
+
else
|
|
63
|
+
# All symbols scan
|
|
64
|
+
SCAN=$($PERP --json arb scan --mode all --min 0 2>/dev/null || echo '{"ok":false}')
|
|
65
|
+
FUNDING_DETAIL=$($PERP --json arb funding 2>/dev/null || echo '{"ok":false}')
|
|
66
|
+
|
|
67
|
+
if $JSON_MODE; then
|
|
68
|
+
cat <<EOF
|
|
69
|
+
{
|
|
70
|
+
"ok": true,
|
|
71
|
+
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
|
|
72
|
+
"topN": $TOP,
|
|
73
|
+
"allOpportunities": $SCAN,
|
|
74
|
+
"fundingDetail": $FUNDING_DETAIL
|
|
75
|
+
}
|
|
76
|
+
EOF
|
|
77
|
+
else
|
|
78
|
+
echo "=== Funding Rate Overview (top $TOP) ==="
|
|
79
|
+
echo ""
|
|
80
|
+
echo "$SCAN" | jq -r "
|
|
81
|
+
.data // [] | .[0:$TOP] | .[] |
|
|
82
|
+
\" \(.symbol) | \(.mode // \"perp\") | \(.netSpread // .spread)bps | \(.longExch // .longExchange)→\(.shortExch // .shortExchange)\"
|
|
83
|
+
" 2>/dev/null || echo "(no data)"
|
|
84
|
+
fi
|
|
85
|
+
fi
|