perp-cli 0.4.2 → 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.
Files changed (68) hide show
  1. package/README.md +2 -0
  2. package/dist/arb/sizing.d.ts +12 -1
  3. package/dist/arb/sizing.js +38 -5
  4. package/dist/arb/state.d.ts +6 -0
  5. package/dist/cli-spec.d.ts +0 -4
  6. package/dist/cli-spec.js +8 -1
  7. package/dist/commands/agent.js +7 -4
  8. package/dist/commands/arb-auto.js +643 -4
  9. package/dist/commands/dex.js +7 -1
  10. package/dist/commands/funding.d.ts +2 -1
  11. package/dist/commands/funding.js +211 -4
  12. package/dist/commands/manage.js +1 -1
  13. package/dist/commands/market.js +1 -1
  14. package/dist/commands/multileg.d.ts +16 -0
  15. package/dist/commands/multileg.js +174 -0
  16. package/dist/commands/pnl.d.ts +12 -0
  17. package/dist/commands/pnl.js +243 -0
  18. package/dist/commands/stream.js +132 -55
  19. package/dist/commands/trade.js +29 -8
  20. package/dist/commands/wallet.js +1 -1
  21. package/dist/config.d.ts +5 -0
  22. package/dist/config.js +12 -0
  23. package/dist/dashboard/ws-feeds.js +1 -1
  24. package/dist/equity-tracker.d.ts +57 -0
  25. package/dist/equity-tracker.js +209 -0
  26. package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
  27. package/dist/exchanges/hyperliquid-spot.js +317 -0
  28. package/dist/exchanges/hyperliquid.d.ts +26 -5
  29. package/dist/exchanges/hyperliquid.js +171 -73
  30. package/dist/exchanges/interface.d.ts +1 -1
  31. package/dist/exchanges/lighter-spot.d.ts +55 -0
  32. package/dist/exchanges/lighter-spot.js +417 -0
  33. package/dist/exchanges/lighter.d.ts +10 -2
  34. package/dist/exchanges/lighter.js +128 -35
  35. package/dist/exchanges/pacifica.d.ts +11 -4
  36. package/dist/exchanges/pacifica.js +52 -37
  37. package/dist/exchanges/spot-interface.d.ts +45 -0
  38. package/dist/exchanges/spot-interface.js +22 -0
  39. package/dist/execution-log.d.ts +2 -2
  40. package/dist/funding/rates.d.ts +36 -0
  41. package/dist/funding/rates.js +146 -0
  42. package/dist/index.js +147 -98
  43. package/dist/liquidity.d.ts +12 -0
  44. package/dist/liquidity.js +43 -0
  45. package/dist/signer/evm-local.d.ts +12 -0
  46. package/dist/signer/evm-local.js +19 -0
  47. package/dist/signer/index.d.ts +3 -0
  48. package/dist/signer/index.js +2 -0
  49. package/dist/signer/interface.d.ts +31 -0
  50. package/dist/signer/interface.js +7 -0
  51. package/dist/signer/solana-local.d.ts +13 -0
  52. package/dist/signer/solana-local.js +19 -0
  53. package/dist/smart-order.js +14 -3
  54. package/dist/utils.js +7 -1
  55. package/dist/ws/market-feed.d.ts +74 -0
  56. package/dist/ws/market-feed.js +227 -0
  57. package/package.json +1 -1
  58. package/skills/perp-cli/SKILL.md +93 -4
  59. package/skills/perp-cli/references/commands.md +16 -1
  60. package/skills/perp-cli/references/strategies.md +1 -1
  61. package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
  62. package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
  63. package/skills/perp-cli/scripts/preflight.sh +98 -0
  64. package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
  65. package/skills/perp-cli/scripts/validate-arb.sh +112 -0
  66. package/skills/perp-cli/templates/arb-scan.sh +0 -15
  67. package/skills/perp-cli/templates/open-position.sh +0 -38
  68. package/skills/perp-cli/templates/wallet-setup.sh +0 -17
@@ -0,0 +1,19 @@
1
+ export class LocalEvmSigner {
2
+ _wallet;
3
+ constructor() { }
4
+ static async create(privateKey) {
5
+ const { ethers } = await import("ethers");
6
+ const signer = new LocalEvmSigner();
7
+ signer._wallet = new ethers.Wallet(privateKey);
8
+ return signer;
9
+ }
10
+ getAddress() {
11
+ return this._wallet.address;
12
+ }
13
+ async signTypedData(domain, types, value) {
14
+ return this._wallet.signTypedData(domain, types, value);
15
+ }
16
+ async signMessage(message) {
17
+ return this._wallet.signMessage(message);
18
+ }
19
+ }
@@ -0,0 +1,3 @@
1
+ export type { EvmSigner, SolanaSigner } from "./interface.js";
2
+ export { LocalEvmSigner } from "./evm-local.js";
3
+ export { LocalSolanaSigner } from "./solana-local.js";
@@ -0,0 +1,2 @@
1
+ export { LocalEvmSigner } from "./evm-local.js";
2
+ export { LocalSolanaSigner } from "./solana-local.js";
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Signer interfaces for exchange adapters.
3
+ *
4
+ * Separates signing logic from adapters so it can be injected
5
+ * (e.g., HSM, hardware wallet, delegated signing).
6
+ */
7
+ /** EVM signer (Hyperliquid + Lighter L1) */
8
+ export interface EvmSigner {
9
+ getAddress(): string;
10
+ /** EIP-712 typed data signing — returns flat hex signature (0x...) */
11
+ signTypedData(domain: Record<string, unknown>, types: Record<string, Array<{
12
+ name: string;
13
+ type: string;
14
+ }>>, value: Record<string, unknown>): Promise<string>;
15
+ /** EIP-191 personal message signing — returns hex signature */
16
+ signMessage(message: string | Uint8Array): Promise<string>;
17
+ }
18
+ /** Solana signer (Pacifica) */
19
+ export interface SolanaSigner {
20
+ getPublicKeyBase58(): string;
21
+ /** Ed25519 detached signature */
22
+ signMessage(message: Uint8Array): Promise<Uint8Array>;
23
+ /** Sign a Solana transaction (full sign) */
24
+ signTransaction(tx: {
25
+ sign(keypair: unknown): void;
26
+ }): void;
27
+ /** Partially sign a Solana transaction */
28
+ partialSignTransaction(tx: {
29
+ partialSign(keypair: unknown): void;
30
+ }): void;
31
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Signer interfaces for exchange adapters.
3
+ *
4
+ * Separates signing logic from adapters so it can be injected
5
+ * (e.g., HSM, hardware wallet, delegated signing).
6
+ */
7
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { SolanaSigner } from "./interface.js";
2
+ export declare class LocalSolanaSigner implements SolanaSigner {
3
+ private _keypair;
4
+ constructor(keypair: import("@solana/web3.js").Keypair);
5
+ getPublicKeyBase58(): string;
6
+ signMessage(message: Uint8Array): Promise<Uint8Array>;
7
+ signTransaction(tx: {
8
+ sign(keypair: unknown): void;
9
+ }): void;
10
+ partialSignTransaction(tx: {
11
+ partialSign(keypair: unknown): void;
12
+ }): void;
13
+ }
@@ -0,0 +1,19 @@
1
+ export class LocalSolanaSigner {
2
+ _keypair;
3
+ constructor(keypair) {
4
+ this._keypair = keypair;
5
+ }
6
+ getPublicKeyBase58() {
7
+ return this._keypair.publicKey.toBase58();
8
+ }
9
+ async signMessage(message) {
10
+ const nacl = await import("tweetnacl");
11
+ return nacl.default.sign.detached(message, this._keypair.secretKey);
12
+ }
13
+ signTransaction(tx) {
14
+ tx.sign(this._keypair);
15
+ }
16
+ partialSignTransaction(tx) {
17
+ tx.partialSign(this._keypair);
18
+ }
19
+ }
@@ -30,15 +30,26 @@ function inferTickSize(levels) {
30
30
  return decimals > 0 ? Math.pow(10, -decimals) : 1;
31
31
  }
32
32
  // Use minimum difference between adjacent price levels
33
+ // Determine max decimal places from the orderbook to round away float noise
34
+ let maxDecimals = 0;
35
+ for (let i = 0; i < Math.min(levels.length, 5); i++) {
36
+ const p = levels[i][0];
37
+ const d = p.includes(".") ? p.split(".")[1].length : 0;
38
+ if (d > maxDecimals)
39
+ maxDecimals = d;
40
+ }
33
41
  let minDiff = Infinity;
34
42
  for (let i = 1; i < Math.min(levels.length, 5); i++) {
35
43
  const diff = Math.abs(Number(levels[i][0]) - Number(levels[i - 1][0]));
36
44
  if (diff > 0 && diff < minDiff)
37
45
  minDiff = diff;
38
46
  }
39
- return minDiff === Infinity
40
- ? Math.pow(10, -(levels[0][0].includes(".") ? levels[0][0].split(".")[1].length : 0))
41
- : minDiff;
47
+ if (minDiff === Infinity) {
48
+ return Math.pow(10, -maxDecimals);
49
+ }
50
+ // Round to orderbook precision to eliminate floating-point noise
51
+ // e.g., 0.10000000000000142 → 0.1
52
+ return Number(minDiff.toFixed(maxDecimals));
42
53
  }
43
54
  /**
44
55
  * Get price precision (decimal places) from a price string.
package/dist/utils.js CHANGED
@@ -107,7 +107,13 @@ export function printJson(data) {
107
107
  const fields = process.argv[fieldsArg + 1].split(",").map(f => f.trim());
108
108
  const envelope = output;
109
109
  if (envelope.ok && envelope.data && typeof envelope.data === "object") {
110
- envelope.data = pickFields(envelope.data, fields);
110
+ // If data is an array, apply pickFields to each element
111
+ if (Array.isArray(envelope.data)) {
112
+ envelope.data = envelope.data.map((item) => pickFields(item, fields));
113
+ }
114
+ else {
115
+ envelope.data = pickFields(envelope.data, fields);
116
+ }
111
117
  }
112
118
  output = envelope;
113
119
  }
@@ -0,0 +1,74 @@
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
+ export interface PriceUpdate {
10
+ symbol: string;
11
+ mid: number;
12
+ funding?: number;
13
+ }
14
+ export interface BookUpdate {
15
+ bids: [string, string][];
16
+ asks: [string, string][];
17
+ }
18
+ export interface TradeUpdate {
19
+ side: string;
20
+ price: string;
21
+ size: string;
22
+ time: number;
23
+ }
24
+ export interface CandleUpdate {
25
+ o: string;
26
+ h: string;
27
+ l: string;
28
+ c: string;
29
+ v: string;
30
+ t: number;
31
+ }
32
+ export declare class HyperliquidMarketFeed extends EventEmitter {
33
+ private ws;
34
+ private closed;
35
+ private reconnectTimer;
36
+ private subs;
37
+ get wsUrl(): string;
38
+ connect(): Promise<void>;
39
+ private send;
40
+ private sub;
41
+ private scheduleReconnect;
42
+ subscribePrices(): void;
43
+ subscribeBook(symbol: string): void;
44
+ subscribeTrades(symbol: string): void;
45
+ subscribeCandle(symbol: string, interval: string): void;
46
+ private handleMessage;
47
+ close(): void;
48
+ }
49
+ export interface LighterFeedOpts {
50
+ /** Polling interval for prices in ms (default: 3000) */
51
+ pricesIntervalMs?: number;
52
+ /** Polling interval for book/trades in ms (default: 2000) */
53
+ bookIntervalMs?: number;
54
+ /** Polling interval for candles in ms (default: 5000) */
55
+ candleIntervalMs?: number;
56
+ }
57
+ export declare class LighterMarketFeed extends EventEmitter {
58
+ private closed;
59
+ private timers;
60
+ private marketMap;
61
+ private pricesMs;
62
+ private bookMs;
63
+ private candleMs;
64
+ constructor(opts?: LighterFeedOpts);
65
+ connect(): Promise<void>;
66
+ private getMarketId;
67
+ private poll;
68
+ subscribePrices(): void;
69
+ subscribeBook(symbol: string): void;
70
+ subscribeTrades(symbol: string): void;
71
+ subscribeCandle(symbol: string, interval: string): void;
72
+ close(): void;
73
+ }
74
+ export declare function createMarketFeed(exchange: string): HyperliquidMarketFeed | LighterMarketFeed;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.4.2",
3
+ "version": "0.4.8",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "dist/index.js",
@@ -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, scan arbitrage opportunities, or mentions perp-cli, hypurrquant, Pacifica, Hyperliquid, or Lighter exchanges."
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.2"
8
+ version: "0.4.7"
9
9
  ---
10
10
 
11
11
  # perp-cli Agent Guide
@@ -100,7 +100,7 @@ perp --json arb close <SYM> --smart # smart arb close
100
100
 
101
101
  All string outputs are auto-sanitized (control chars stripped, prompt injection patterns blocked).
102
102
 
103
- ## Arb Workflow
103
+ ## Arb Workflow (Perp-Perp)
104
104
 
105
105
  ```
106
106
  1. perp --json portfolio → check balances across all exchanges
@@ -119,6 +119,39 @@ All string outputs are auto-sanitized (control chars stripped, prompt injection
119
119
  → --pair <longEx>:<shortEx>: specify which pair if multiple
120
120
  ```
121
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
+
122
155
  ## Position Sizing
123
156
 
124
157
  - Single position < **80%** of that exchange's available balance
@@ -176,6 +209,39 @@ If `error.retryable` is `false`, do NOT retry — fix the cause first.
176
209
  | `DUPLICATE_ORDER` | already submitted — check positions, don't retry |
177
210
  | Lighter `invalid signature` | check ~/.perp/.env or `perp --json -e lighter manage setup-api-key` |
178
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
+
179
245
  ## Referrals (Optional)
180
246
 
181
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.
@@ -188,6 +254,29 @@ perp --json settings referrals off # disable anytime
188
254
  When setting up wallets for the first time, ask the user:
189
255
  > "perp-cli 개발을 지원하는 레퍼럴 기능을 켜시겠어요? 추가 수수료 없이 거래소 리베이트만 활용됩니다."
190
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
+
191
280
  ## References
192
281
 
193
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> # tick size, min order, max leverage
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 minOrderSize, stepSize
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