perp-cli 0.3.19 → 0.4.2
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 +4 -1
- package/dist/arb/utils.d.ts +2 -0
- package/dist/arb/utils.js +1 -1
- package/dist/commands/agent.js +19 -11
- package/dist/commands/arb/index.js +288 -31
- package/dist/commands/arb-auto.js +242 -63
- package/dist/commands/init.js +1 -1
- package/dist/commands/settings.d.ts +2 -1
- package/dist/commands/settings.js +133 -2
- package/dist/commands/trade.js +89 -35
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +40 -0
- package/dist/exchanges/hyperliquid.js +13 -22
- package/dist/exchanges/lighter.js +50 -3
- package/dist/exchanges/pacifica.js +1 -1
- package/dist/index.js +7 -7
- package/dist/mcp-server.js +6 -6
- package/dist/settings.d.ts +6 -0
- package/dist/settings.js +18 -2
- package/dist/smart-order.d.ts +44 -0
- package/dist/smart-order.js +134 -0
- package/dist/trade-validator.js +2 -1
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +31 -8
package/dist/index.js
CHANGED
|
@@ -297,7 +297,7 @@ registerPortfolioCommands(program, getAdapterForExchange, isJson);
|
|
|
297
297
|
registerRiskCommands(program, getAdapterForExchange, isJson);
|
|
298
298
|
registerHistoryCommands(program, isJson);
|
|
299
299
|
registerAnalyticsCommands(program, getAdapterForExchange, isJson);
|
|
300
|
-
registerSettingsCommands(program, isJson);
|
|
300
|
+
registerSettingsCommands(program, isJson, getAdapterForExchange);
|
|
301
301
|
registerDexCommands(program, getAdapter, isJson);
|
|
302
302
|
registerPlanCommands(program, getAdapter, isJson);
|
|
303
303
|
registerFundingCommands(program, isJson);
|
|
@@ -388,20 +388,20 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
388
388
|
process.env.LIGHTER_PRIVATE_KEY);
|
|
389
389
|
if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
|
|
390
390
|
// Fresh install — onboarding
|
|
391
|
-
console.log(chalk.cyan.bold("\n Welcome to perp-cli!") + chalk.gray(
|
|
391
|
+
console.log(chalk.cyan.bold("\n Welcome to perp-cli!") + chalk.gray(` v${_pkg.version}\n`));
|
|
392
392
|
console.log(" Multi-DEX perpetual futures CLI for Pacifica, Hyperliquid, and Lighter.\n");
|
|
393
|
-
console.log(` Get started: ${chalk.cyan("perp
|
|
393
|
+
console.log(` Get started: ${chalk.cyan("perp wallet set <exchange> <key>")}`);
|
|
394
394
|
console.log(chalk.gray(`\n Or explore without a wallet:`));
|
|
395
395
|
console.log(` ${chalk.green("perp market list")} available markets`);
|
|
396
396
|
console.log(` ${chalk.green("perp -e hyperliquid market list")} Hyperliquid markets`);
|
|
397
|
-
console.log(` ${chalk.green("perp arb
|
|
397
|
+
console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
|
|
398
398
|
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
399
399
|
}
|
|
400
400
|
else {
|
|
401
401
|
// Configured — show status overview
|
|
402
402
|
const defaultEx = settings.defaultExchange || "pacifica";
|
|
403
403
|
const activeEntries = Object.entries(status.active);
|
|
404
|
-
console.log(chalk.cyan.bold("\n perp-cli") + chalk.gray(
|
|
404
|
+
console.log(chalk.cyan.bold("\n perp-cli") + chalk.gray(` v${_pkg.version}\n`));
|
|
405
405
|
console.log(` Default exchange: ${chalk.cyan(defaultEx)}`);
|
|
406
406
|
if (activeEntries.length > 0) {
|
|
407
407
|
console.log(chalk.white.bold("\n Wallets:"));
|
|
@@ -432,10 +432,10 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
432
432
|
}
|
|
433
433
|
}
|
|
434
434
|
console.log(chalk.white.bold("\n Quick commands:"));
|
|
435
|
-
console.log(` ${chalk.green("perp
|
|
435
|
+
console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
|
|
436
436
|
console.log(` ${chalk.green("perp market list")} available markets`);
|
|
437
|
+
console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
|
|
437
438
|
console.log(` ${chalk.green("perp dashboard")} live monitoring`);
|
|
438
|
-
console.log(` ${chalk.green("perp arb rates")} funding rate comparison`);
|
|
439
439
|
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
440
440
|
}
|
|
441
441
|
}
|
package/dist/mcp-server.js
CHANGED
|
@@ -333,7 +333,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
333
333
|
steps.push({ step: 1, command: `perp -e ${ex} --json account orders`, description: "List open orders to find order ID" }, { step: 2, command: `perp -e ${ex} --json trade edit <symbol> <orderId> <newPrice> <newSize>`, description: "Modify the order", dangerous: true });
|
|
334
334
|
}
|
|
335
335
|
else if (g.includes("arb") || g.includes("arbitrage")) {
|
|
336
|
-
steps.push({ step: 1, command: "perp --json arb
|
|
336
|
+
steps.push({ step: 1, command: "perp --json arb scan --min 5", description: "Scan funding rate arbitrage opportunities" }, { step: 2, command: "perp --json arb scan", description: "Find high-spread opportunities" }, { step: 3, command: "perp --json gap show", description: "Check cross-exchange price gaps" }, { step: 4, command: "perp --json arb dex", description: "HIP-3 cross-dex arb opportunities (Hyperliquid)" });
|
|
337
337
|
}
|
|
338
338
|
else if (g.includes("funding")) {
|
|
339
339
|
const symbol = extractSymbol(g);
|
|
@@ -712,7 +712,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
712
712
|
parameters: [],
|
|
713
713
|
risks: [],
|
|
714
714
|
category: "read",
|
|
715
|
-
relatedCommands: ["perp
|
|
715
|
+
relatedCommands: ["perp portfolio", "perp portfolio"],
|
|
716
716
|
};
|
|
717
717
|
}
|
|
718
718
|
else if (category === "arb") {
|
|
@@ -731,7 +731,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
731
731
|
parameters: [],
|
|
732
732
|
risks: ["auto" === sub ? "Auto-executes trades on both exchanges" : ""].filter(Boolean),
|
|
733
733
|
category: sub === "auto" || sub === "close" ? "write" : "analysis",
|
|
734
|
-
relatedCommands: ["perp arb
|
|
734
|
+
relatedCommands: ["perp arb scan", "perp arb scan", "perp gap show"],
|
|
735
735
|
};
|
|
736
736
|
}
|
|
737
737
|
else if (category === "bridge") {
|
|
@@ -837,7 +837,7 @@ server.tool("explain_command", "Explain what a perp CLI command does, its parame
|
|
|
837
837
|
parameters: [],
|
|
838
838
|
risks: [],
|
|
839
839
|
category: "read",
|
|
840
|
-
relatedCommands: ["perp funding rates", "perp arb
|
|
840
|
+
relatedCommands: ["perp funding rates", "perp arb scan"],
|
|
841
841
|
};
|
|
842
842
|
}
|
|
843
843
|
else if (category === "backtest") {
|
|
@@ -1028,7 +1028,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1028
1028
|
arb: {
|
|
1029
1029
|
description: "Funding rate arbitrage & basis trading",
|
|
1030
1030
|
subcommands: {
|
|
1031
|
-
rates: { usage: "perp arb
|
|
1031
|
+
rates: { usage: "perp arb scan", description: "Compare funding rates" },
|
|
1032
1032
|
scan: { usage: "perp arb scan --min <bps>", description: "Find arb opportunities" },
|
|
1033
1033
|
funding: { usage: "perp arb funding", description: "Detailed funding analysis" },
|
|
1034
1034
|
dex: { usage: "perp arb dex", description: "HIP-3 cross-dex arb" },
|
|
@@ -1167,7 +1167,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1167
1167
|
deposit: { usage: "perp deposit <exchange> <amount>", description: "Deposit USDC to exchange" },
|
|
1168
1168
|
withdraw: { usage: "perp withdraw <exchange> <amount>", description: "Withdraw USDC from exchange" },
|
|
1169
1169
|
portfolio: { usage: "perp portfolio", description: "Cross-exchange portfolio summary" },
|
|
1170
|
-
status: { usage: "perp
|
|
1170
|
+
status: { usage: "perp portfolio", description: "Full account overview" },
|
|
1171
1171
|
health: { usage: "perp health", description: "Exchange connectivity check" },
|
|
1172
1172
|
settings: {
|
|
1173
1173
|
description: "CLI settings",
|
package/dist/settings.d.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export interface ExchangeFees {
|
|
2
|
+
taker: number;
|
|
3
|
+
maker: number;
|
|
4
|
+
}
|
|
1
5
|
export interface Settings {
|
|
2
6
|
/** Default exchange when -e flag is omitted */
|
|
3
7
|
defaultExchange: string;
|
|
@@ -14,6 +18,8 @@ export interface Settings {
|
|
|
14
18
|
hyperliquid: boolean;
|
|
15
19
|
lighter: boolean;
|
|
16
20
|
};
|
|
21
|
+
/** Per-exchange fee tiers (fetched from exchange APIs) */
|
|
22
|
+
fees: Record<string, ExchangeFees>;
|
|
17
23
|
}
|
|
18
24
|
export declare function loadSettings(): Settings;
|
|
19
25
|
export declare function saveSettings(settings: Settings): void;
|
package/dist/settings.js
CHANGED
|
@@ -14,12 +14,27 @@ const DEFAULTS = {
|
|
|
14
14
|
hyperliquid: false,
|
|
15
15
|
lighter: false,
|
|
16
16
|
},
|
|
17
|
+
fees: {
|
|
18
|
+
hyperliquid: { taker: 0.00035, maker: 0.00002 },
|
|
19
|
+
pacifica: { taker: 0.00035, maker: 0.0001 },
|
|
20
|
+
lighter: { taker: 0, maker: 0 },
|
|
21
|
+
},
|
|
17
22
|
};
|
|
18
23
|
export function loadSettings() {
|
|
19
24
|
if (!existsSync(SETTINGS_FILE))
|
|
20
|
-
return { ...DEFAULTS, referralCodes: { ...DEFAULTS.referralCodes } };
|
|
25
|
+
return { ...DEFAULTS, referralCodes: { ...DEFAULTS.referralCodes }, fees: { ...DEFAULTS.fees } };
|
|
21
26
|
try {
|
|
22
27
|
const stored = JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
|
|
28
|
+
// Merge stored fees with defaults (so new exchanges get defaults)
|
|
29
|
+
const fees = { ...DEFAULTS.fees };
|
|
30
|
+
if (stored.fees && typeof stored.fees === "object") {
|
|
31
|
+
for (const [ex, f] of Object.entries(stored.fees)) {
|
|
32
|
+
const fee = f;
|
|
33
|
+
if (fee && typeof fee.taker === "number" && typeof fee.maker === "number") {
|
|
34
|
+
fees[ex] = { taker: fee.taker, maker: fee.maker };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
23
38
|
return {
|
|
24
39
|
defaultExchange: stored.defaultExchange ?? DEFAULTS.defaultExchange,
|
|
25
40
|
referrals: stored.referrals ?? DEFAULTS.referrals,
|
|
@@ -32,10 +47,11 @@ export function loadSettings() {
|
|
|
32
47
|
hyperliquid: stored.referralApplied?.hyperliquid ?? DEFAULTS.referralApplied.hyperliquid,
|
|
33
48
|
lighter: stored.referralApplied?.lighter ?? DEFAULTS.referralApplied.lighter,
|
|
34
49
|
},
|
|
50
|
+
fees,
|
|
35
51
|
};
|
|
36
52
|
}
|
|
37
53
|
catch {
|
|
38
|
-
return { ...DEFAULTS, referralCodes: { ...DEFAULTS.referralCodes } };
|
|
54
|
+
return { ...DEFAULTS, referralCodes: { ...DEFAULTS.referralCodes }, fees: { ...DEFAULTS.fees } };
|
|
39
55
|
}
|
|
40
56
|
}
|
|
41
57
|
export function saveSettings(settings) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Order Execution — best bid/ask + 1 tick pricing.
|
|
3
|
+
*
|
|
4
|
+
* Instead of `marketOrder()` which can sweep deep into the book (Hyperliquid
|
|
5
|
+
* DEX uses 5% slippage by default), this fetches the orderbook and places an
|
|
6
|
+
* order at exactly the best available price plus one tick.
|
|
7
|
+
*
|
|
8
|
+
* For buys: limit price = best ask + 1 tick (fills at best ask or better)
|
|
9
|
+
* For sells: limit price = best bid - 1 tick (fills at best bid or better)
|
|
10
|
+
*
|
|
11
|
+
* Uses IOC (Immediate-Or-Cancel) so the order fills instantly at the best
|
|
12
|
+
* level and any unfilled remainder is cancelled — no resting orders left.
|
|
13
|
+
*
|
|
14
|
+
* Benefits:
|
|
15
|
+
* - Fills only at the top-of-book price (no multi-level sweep)
|
|
16
|
+
* - Saves 5-50+ bps vs raw market orders on thin books
|
|
17
|
+
* - Predictable execution price
|
|
18
|
+
*
|
|
19
|
+
* Fallback: if the IOC limit fails, falls back to market order.
|
|
20
|
+
*/
|
|
21
|
+
import type { ExchangeAdapter } from "./exchanges/interface.js";
|
|
22
|
+
export interface SmartOrderOpts {
|
|
23
|
+
/** Extra ticks of tolerance beyond best price. Default: 1 */
|
|
24
|
+
tickTolerance?: number;
|
|
25
|
+
/** Fall back to market order if IOC limit fails. Default: true */
|
|
26
|
+
fallback?: boolean;
|
|
27
|
+
/** Reduce only flag for closing positions. Default: false */
|
|
28
|
+
reduceOnly?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface SmartOrderResult {
|
|
31
|
+
result: unknown;
|
|
32
|
+
method: "limit_ioc" | "market_fallback";
|
|
33
|
+
/** The actual limit price used for the order */
|
|
34
|
+
price: string;
|
|
35
|
+
/** The best price from the orderbook */
|
|
36
|
+
bestBookPrice: string;
|
|
37
|
+
/** Tick size inferred from orderbook */
|
|
38
|
+
tickSize: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Execute a smart order at best bid/ask + 1 tick.
|
|
42
|
+
* Drop-in replacement for `adapter.marketOrder()`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function smartOrder(adapter: ExchangeAdapter, symbol: string, side: "buy" | "sell", size: string, opts?: SmartOrderOpts): Promise<SmartOrderResult>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Smart Order Execution — best bid/ask + 1 tick pricing.
|
|
3
|
+
*
|
|
4
|
+
* Instead of `marketOrder()` which can sweep deep into the book (Hyperliquid
|
|
5
|
+
* DEX uses 5% slippage by default), this fetches the orderbook and places an
|
|
6
|
+
* order at exactly the best available price plus one tick.
|
|
7
|
+
*
|
|
8
|
+
* For buys: limit price = best ask + 1 tick (fills at best ask or better)
|
|
9
|
+
* For sells: limit price = best bid - 1 tick (fills at best bid or better)
|
|
10
|
+
*
|
|
11
|
+
* Uses IOC (Immediate-Or-Cancel) so the order fills instantly at the best
|
|
12
|
+
* level and any unfilled remainder is cancelled — no resting orders left.
|
|
13
|
+
*
|
|
14
|
+
* Benefits:
|
|
15
|
+
* - Fills only at the top-of-book price (no multi-level sweep)
|
|
16
|
+
* - Saves 5-50+ bps vs raw market orders on thin books
|
|
17
|
+
* - Predictable execution price
|
|
18
|
+
*
|
|
19
|
+
* Fallback: if the IOC limit fails, falls back to market order.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Infer tick size from orderbook prices.
|
|
23
|
+
* Uses the minimum price increment visible in the book.
|
|
24
|
+
*/
|
|
25
|
+
function inferTickSize(levels) {
|
|
26
|
+
if (levels.length < 2) {
|
|
27
|
+
// Single level: infer from decimal places (e.g., "1.234" → tick = 0.001)
|
|
28
|
+
const price = levels[0]?.[0] ?? "1";
|
|
29
|
+
const decimals = price.includes(".") ? price.split(".")[1].length : 0;
|
|
30
|
+
return decimals > 0 ? Math.pow(10, -decimals) : 1;
|
|
31
|
+
}
|
|
32
|
+
// Use minimum difference between adjacent price levels
|
|
33
|
+
let minDiff = Infinity;
|
|
34
|
+
for (let i = 1; i < Math.min(levels.length, 5); i++) {
|
|
35
|
+
const diff = Math.abs(Number(levels[i][0]) - Number(levels[i - 1][0]));
|
|
36
|
+
if (diff > 0 && diff < minDiff)
|
|
37
|
+
minDiff = diff;
|
|
38
|
+
}
|
|
39
|
+
return minDiff === Infinity
|
|
40
|
+
? Math.pow(10, -(levels[0][0].includes(".") ? levels[0][0].split(".")[1].length : 0))
|
|
41
|
+
: minDiff;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Get price precision (decimal places) from a price string.
|
|
45
|
+
*/
|
|
46
|
+
function pricePrecision(priceStr) {
|
|
47
|
+
return priceStr.includes(".") ? priceStr.split(".")[1].length : 0;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if an order response contains an embedded error (not thrown).
|
|
51
|
+
* Some exchanges (e.g., Hyperliquid) return success at HTTP level but include
|
|
52
|
+
* error statuses inside the response body.
|
|
53
|
+
*/
|
|
54
|
+
function hasOrderError(result) {
|
|
55
|
+
if (!result || typeof result !== "object")
|
|
56
|
+
return false;
|
|
57
|
+
const r = result;
|
|
58
|
+
// Hyperliquid: { response: { data: { statuses: [{ error: "..." }] } } }
|
|
59
|
+
const resp = r.response;
|
|
60
|
+
const data = resp?.data;
|
|
61
|
+
const statuses = data?.statuses;
|
|
62
|
+
if (Array.isArray(statuses) && statuses.length > 0 && statuses[0].error)
|
|
63
|
+
return true;
|
|
64
|
+
// Generic: { ok: false } or { success: false }
|
|
65
|
+
if (r.ok === false || r.success === false)
|
|
66
|
+
return true;
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Execute a smart order at best bid/ask + 1 tick.
|
|
71
|
+
* Drop-in replacement for `adapter.marketOrder()`.
|
|
72
|
+
*/
|
|
73
|
+
export async function smartOrder(adapter, symbol, side, size, opts = {}) {
|
|
74
|
+
const { tickTolerance = 1, fallback = true, reduceOnly = false } = opts;
|
|
75
|
+
// 1. Fetch orderbook
|
|
76
|
+
const book = await adapter.getOrderbook(symbol);
|
|
77
|
+
// 2. Validate book has liquidity on the relevant side
|
|
78
|
+
const relevantSide = side === "buy" ? book.asks : book.bids;
|
|
79
|
+
if (relevantSide.length === 0) {
|
|
80
|
+
if (!fallback)
|
|
81
|
+
throw new Error(`No ${side === "buy" ? "asks" : "bids"} in orderbook for ${symbol}`);
|
|
82
|
+
const result = await adapter.marketOrder(symbol, side, size);
|
|
83
|
+
return { result, method: "market_fallback", price: "0", bestBookPrice: "0", tickSize: "0" };
|
|
84
|
+
}
|
|
85
|
+
// 3. Get best price and tick size
|
|
86
|
+
const bestEntry = relevantSide[0];
|
|
87
|
+
const bestPrice = Number(bestEntry[0]);
|
|
88
|
+
const tick = inferTickSize(relevantSide);
|
|
89
|
+
const decimals = pricePrecision(bestEntry[0]);
|
|
90
|
+
// 4. Calculate limit price: best price + N ticks in favorable direction
|
|
91
|
+
const limitPrice = side === "buy"
|
|
92
|
+
? bestPrice + tick * tickTolerance // 1 tick above best ask
|
|
93
|
+
: bestPrice - tick * tickTolerance; // 1 tick below best bid
|
|
94
|
+
const formattedPrice = limitPrice.toFixed(decimals);
|
|
95
|
+
// 5. Place IOC limit order at calculated price
|
|
96
|
+
try {
|
|
97
|
+
const result = await adapter.limitOrder(symbol, side, formattedPrice, size, {
|
|
98
|
+
tif: "IOC",
|
|
99
|
+
reduceOnly,
|
|
100
|
+
});
|
|
101
|
+
// Check for exchange-level rejection inside a "success" response
|
|
102
|
+
// (e.g., Hyperliquid returns { status: "ok", response: { data: { statuses: [{ error: "..." }] } } })
|
|
103
|
+
if (fallback && hasOrderError(result)) {
|
|
104
|
+
const fallbackResult = await adapter.marketOrder(symbol, side, size);
|
|
105
|
+
return {
|
|
106
|
+
result: fallbackResult,
|
|
107
|
+
method: "market_fallback",
|
|
108
|
+
price: formattedPrice,
|
|
109
|
+
bestBookPrice: bestEntry[0],
|
|
110
|
+
tickSize: tick.toFixed(decimals),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
result,
|
|
115
|
+
method: "limit_ioc",
|
|
116
|
+
price: formattedPrice,
|
|
117
|
+
bestBookPrice: bestEntry[0],
|
|
118
|
+
tickSize: tick.toFixed(decimals),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (!fallback)
|
|
123
|
+
throw err;
|
|
124
|
+
// Fallback to raw market order for reliability
|
|
125
|
+
const result = await adapter.marketOrder(symbol, side, size);
|
|
126
|
+
return {
|
|
127
|
+
result,
|
|
128
|
+
method: "market_fallback",
|
|
129
|
+
price: formattedPrice,
|
|
130
|
+
bestBookPrice: bestEntry[0],
|
|
131
|
+
tickSize: tick.toFixed(decimals),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
package/dist/trade-validator.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { symbolMatch } from "./utils.js";
|
|
2
|
-
|
|
2
|
+
import { DEFAULT_TAKER_FEE } from "./constants.js";
|
|
3
|
+
// Use centralized DEFAULT_TAKER_FEE from constants.ts (0.035%)
|
|
3
4
|
const DEFAULT_MAX_SLIPPAGE = 0.005; // 0.5%
|
|
4
5
|
/**
|
|
5
6
|
* Validate a trade before execution.
|
package/package.json
CHANGED
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
|
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
7
|
author: hypurrquant
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.4.2"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# perp-cli Agent Guide
|
|
@@ -21,12 +21,21 @@ Multi-DEX perpetual futures CLI — Pacifica (Solana), Hyperliquid (HyperEVM), L
|
|
|
21
21
|
5. **NEVER trade without user confirmation.**
|
|
22
22
|
6. **NEVER read ~/.perp/.env or key files.**
|
|
23
23
|
|
|
24
|
-
## Install
|
|
24
|
+
## Install (run this FIRST)
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
|
|
27
|
+
# 1. Check if perp exists and is current (must be ≥ 0.3.19)
|
|
28
|
+
perp --version 2>/dev/null
|
|
29
|
+
|
|
30
|
+
# 2. If not found or outdated:
|
|
31
|
+
npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
|
|
32
|
+
|
|
33
|
+
# 3. If version is still old (npx cache), clear cache first:
|
|
34
|
+
npx -y clear-npx-cache 2>/dev/null; npx -y perp-cli@latest --json --version
|
|
28
35
|
```
|
|
29
|
-
|
|
36
|
+
|
|
37
|
+
**IMPORTANT:** Always use `@latest` with npx — without it, npx uses a stale cached version.
|
|
38
|
+
Use `perp` if global install works, otherwise `npx -y perp-cli@latest` as prefix for all commands.
|
|
30
39
|
|
|
31
40
|
## Wallet Setup
|
|
32
41
|
|
|
@@ -50,7 +59,9 @@ perp --json arb exec <SYM> <longEx> <shortEx> <$> --leverage <N> --isolated # e
|
|
|
50
59
|
|
|
51
60
|
# Single exchange trading (when not doing arb)
|
|
52
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)
|
|
53
63
|
perp --json -e <EX> trade close <SYM>
|
|
64
|
+
perp --json -e <EX> trade close <SYM> --smart # smart close at best bid/ask
|
|
54
65
|
|
|
55
66
|
# Risk (only if you need to set limits — portfolio already includes risk)
|
|
56
67
|
perp --json risk limits --max-leverage 5
|
|
@@ -78,6 +89,13 @@ perp --json --ndjson -e <EX> market list
|
|
|
78
89
|
|
|
79
90
|
# Prevent duplicate orders with client ID
|
|
80
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
|
|
81
99
|
```
|
|
82
100
|
|
|
83
101
|
All string outputs are auto-sanitized (control chars stripped, prompt injection patterns blocked).
|
|
@@ -94,7 +112,11 @@ All string outputs are auto-sanitized (control chars stripped, prompt injection
|
|
|
94
112
|
→ executes BOTH legs simultaneously
|
|
95
113
|
→ verifies positions exist after execution
|
|
96
114
|
→ auto-rollback if one leg fails
|
|
97
|
-
5. perp --json
|
|
115
|
+
5. perp --json arb status → monitor: PnL, funding income, daily estimate, breakeven
|
|
116
|
+
6. perp --json arb funding-earned → actual funding payments received/paid + APR
|
|
117
|
+
7. perp --json arb close <SYM> → close both legs simultaneously (retry + verify)
|
|
118
|
+
→ --dry-run: preview without executing
|
|
119
|
+
→ --pair <longEx>:<shortEx>: specify which pair if multiple
|
|
98
120
|
```
|
|
99
121
|
|
|
100
122
|
## Position Sizing
|
|
@@ -128,10 +150,11 @@ Check `canTrade` before any order. If `false`, do NOT trade.
|
|
|
128
150
|
## Monitoring (while positions open)
|
|
129
151
|
|
|
130
152
|
```
|
|
131
|
-
Every 15 min: perp --json
|
|
132
|
-
Every 1 hour: perp --json arb scan --min 5
|
|
153
|
+
Every 15 min: perp --json arb status → PnL, daily income, breakeven analysis
|
|
154
|
+
Every 1 hour: perp --json arb scan --min 5 → check if spread still profitable
|
|
155
|
+
Daily: perp --json arb funding-earned → verify actual funding payments
|
|
133
156
|
Exit if: spread < breakeven or one leg closed unexpectedly.
|
|
134
|
-
Close
|
|
157
|
+
Close: perp --json arb close <SYM> → close both legs with retry + verification
|
|
135
158
|
```
|
|
136
159
|
|
|
137
160
|
## Error Handling
|