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.
- 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/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +7 -4
- package/dist/commands/arb-auto.js +643 -4
- 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/stream.js +132 -55
- package/dist/commands/trade.js +29 -8
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -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 +171 -73
- 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 +128 -35
- 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 +147 -98
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- 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.js +14 -3
- 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 +93 -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,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,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,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
|
+
}
|
package/dist/smart-order.js
CHANGED
|
@@ -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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
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
|
|
@@ -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> #
|
|
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
|
|