perp-cli 0.3.3 → 0.3.4

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 CHANGED
@@ -178,21 +178,25 @@ perp alert daemon --interval 30
178
178
 
179
179
  ## Claude Code Agent Skill
180
180
 
181
- Install as a Claude Code plugin to let Claude trade for you:
181
+ Install as a Claude Code plugin or via the universal skills CLI:
182
182
 
183
183
  ```bash
184
- # In Claude Code
184
+ # Universal skill installer (works with Claude Code, Cursor, Codex, Gemini CLI, etc.)
185
+ npx skills add hypurrquant/perp-cli
186
+
187
+ # Or in Claude Code directly
185
188
  /plugin marketplace add hypurrquant/perp-cli
186
- /plugin install perp-trading@hypurrquant-perp-cli
189
+ /plugin install perp-cli
187
190
  ```
188
191
 
189
- Once installed, use `/perp-trading` in Claude Code:
192
+ Once installed, Claude can trade for you via natural language:
190
193
 
191
194
  ```
192
- /perp-trading status # Check all exchanges
193
- /perp-trading BTC 0.01 long on hyperliquid # Natural language trading
194
- /perp-trading scan arb opportunities # Find funding rate arb
195
- /perp-trading bridge 100 USDC solana to arb # Cross-chain bridge
195
+ perp --json wallet show # Check configured wallets
196
+ perp --json -e hl market list # Browse markets
197
+ perp --json -e hl trade buy BTC 0.01 # Execute trades
198
+ perp --json arb rates # Find funding rate arb
199
+ perp --json bridge quote --from solana --to arbitrum --amount 100
196
200
  ```
197
201
 
198
202
  The skill includes safety guardrails (balance checks, user confirmation before trades, error handling with retries).
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { assessRisk, preTradeCheck } from "../risk.js";
2
+ import { assessRisk, preTradeCheck, calcLiquidationDistance, getLiquidationDistances, LIQUIDATION_DISTANCE_HARD_CAP } from "../risk.js";
3
3
  const defaultLimits = {
4
4
  maxDrawdownUsd: 500,
5
5
  maxPositionUsd: 5000,
@@ -8,12 +8,13 @@ const defaultLimits = {
8
8
  maxPositions: 10,
9
9
  maxLeverage: 20,
10
10
  maxMarginUtilization: 80,
11
+ minLiquidationDistance: 30,
11
12
  };
12
13
  function makeBalance(equity, available, marginUsed, pnl) {
13
14
  return { equity: String(equity), available: String(available), marginUsed: String(marginUsed), unrealizedPnl: String(pnl) };
14
15
  }
15
- function makePosition(symbol, side, size, markPrice, leverage, pnl) {
16
- return { symbol, side, size: String(size), entryPrice: String(markPrice), markPrice: String(markPrice), liquidationPrice: "0", unrealizedPnl: String(pnl), leverage };
16
+ function makePosition(symbol, side, size, markPrice, leverage, pnl, liquidationPrice = "0") {
17
+ return { symbol, side, size: String(size), entryPrice: String(markPrice), markPrice: String(markPrice), liquidationPrice, unrealizedPnl: String(pnl), leverage };
17
18
  }
18
19
  describe("Risk Assessment", () => {
19
20
  it("should return low risk when everything is within limits", () => {
@@ -143,3 +144,123 @@ describe("Pre-Trade Check", () => {
143
144
  expect(result.reason).toContain("Leverage");
144
145
  });
145
146
  });
147
+ describe("calcLiquidationDistance", () => {
148
+ it("should calculate distance for long position (liq below mark)", () => {
149
+ // Long at $100000, liq at $80000 → 20% distance
150
+ const dist = calcLiquidationDistance(100000, 80000, "long");
151
+ expect(dist).toBeCloseTo(20, 1);
152
+ });
153
+ it("should calculate distance for short position (liq above mark)", () => {
154
+ // Short at $100000, liq at $120000 → 20% distance
155
+ const dist = calcLiquidationDistance(100000, 120000, "short");
156
+ expect(dist).toBeCloseTo(20, 1);
157
+ });
158
+ it("should return Infinity when liquidation price is 0 or N/A", () => {
159
+ expect(calcLiquidationDistance(100000, 0, "long")).toBe(Infinity);
160
+ expect(calcLiquidationDistance(0, 80000, "long")).toBe(Infinity);
161
+ });
162
+ it("should handle very close liquidation (dangerous)", () => {
163
+ // Long at $100000, liq at $95000 → 5% distance
164
+ const dist = calcLiquidationDistance(100000, 95000, "long");
165
+ expect(dist).toBeCloseTo(5, 1);
166
+ });
167
+ it("should handle very safe distance", () => {
168
+ // Long at $100000, liq at $50000 → 50% distance
169
+ const dist = calcLiquidationDistance(100000, 50000, "long");
170
+ expect(dist).toBeCloseTo(50, 1);
171
+ });
172
+ });
173
+ describe("getLiquidationDistances", () => {
174
+ it("should return sorted by distance (closest first)", () => {
175
+ const positions = [
176
+ { exchange: "hl", position: makePosition("ETH", "long", 1, 3500, 5, 0, "3000") }, // ~14.3%
177
+ { exchange: "pac", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "50000") }, // 50%
178
+ { exchange: "hl", position: makePosition("SOL", "short", 10, 150, 10, 0, "165") }, // 10%
179
+ ];
180
+ const distances = getLiquidationDistances(positions, defaultLimits);
181
+ expect(distances.length).toBe(3);
182
+ expect(distances[0].symbol).toBe("SOL"); // closest to liq
183
+ expect(distances[1].symbol).toBe("ETH");
184
+ expect(distances[2].symbol).toBe("BTC"); // safest
185
+ });
186
+ it("should assign correct status based on limits", () => {
187
+ const limits = { ...defaultLimits, minLiquidationDistance: 30 };
188
+ const positions = [
189
+ { exchange: "a", position: makePosition("A", "long", 1, 100, 2, 0, "85") }, // 15% → critical (< 20% hard cap)
190
+ { exchange: "b", position: makePosition("B", "long", 1, 100, 2, 0, "78") }, // 22% → danger (< 30% user limit)
191
+ { exchange: "c", position: makePosition("C", "long", 1, 100, 2, 0, "65") }, // 35% → warning (< 30% * 1.5 = 45%)
192
+ { exchange: "d", position: makePosition("D", "long", 1, 100, 2, 0, "40") }, // 60% → safe
193
+ ];
194
+ const distances = getLiquidationDistances(positions, limits);
195
+ expect(distances.find(d => d.symbol === "A").status).toBe("critical");
196
+ expect(distances.find(d => d.symbol === "B").status).toBe("danger");
197
+ expect(distances.find(d => d.symbol === "C").status).toBe("warning");
198
+ expect(distances.find(d => d.symbol === "D").status).toBe("safe");
199
+ });
200
+ it("should skip positions with liquidationPrice 0 or N/A", () => {
201
+ const positions = [
202
+ { exchange: "a", position: makePosition("BTC", "long", 1, 100000, 5, 0, "0") },
203
+ { exchange: "b", position: makePosition("ETH", "long", 1, 3500, 5, 0, "N/A") },
204
+ { exchange: "c", position: makePosition("SOL", "long", 1, 150, 5, 0, "120") }, // valid
205
+ ];
206
+ const distances = getLiquidationDistances(positions, defaultLimits);
207
+ expect(distances.length).toBe(1);
208
+ expect(distances[0].symbol).toBe("SOL");
209
+ });
210
+ });
211
+ describe("Liquidation Distance in assessRisk", () => {
212
+ it("should add critical violation when position is below hard cap (20%)", () => {
213
+ const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
214
+ // BTC long at $100000, liq at $90000 → 10% distance (below 20% hard cap)
215
+ const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 10, 0, "90000") }];
216
+ const result = assessRisk(balances, positions, defaultLimits);
217
+ expect(result.violations.some(v => v.rule === "liquidation_distance_hard_cap")).toBe(true);
218
+ expect(result.level).toBe("critical");
219
+ expect(result.canTrade).toBe(false);
220
+ });
221
+ it("should add high violation when below user limit but above hard cap", () => {
222
+ const limits = { ...defaultLimits, minLiquidationDistance: 30 };
223
+ const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
224
+ // BTC long at $100000, liq at $75000 → 25% distance (above 20% hard cap, below 30% user limit)
225
+ const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 4, 0, "75000") }];
226
+ const result = assessRisk(balances, positions, limits);
227
+ expect(result.violations.some(v => v.rule === "min_liquidation_distance")).toBe(true);
228
+ expect(result.violations.some(v => v.rule === "liquidation_distance_hard_cap")).toBe(false);
229
+ });
230
+ it("should not add violation when distance is above user limit", () => {
231
+ const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
232
+ // BTC long at $100000, liq at $50000 → 50% distance (safe)
233
+ const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "50000") }];
234
+ const result = assessRisk(balances, positions, defaultLimits);
235
+ expect(result.violations.some(v => v.rule === "liquidation_distance_hard_cap")).toBe(false);
236
+ expect(result.violations.some(v => v.rule === "min_liquidation_distance")).toBe(false);
237
+ });
238
+ it("should report minLiquidationDistancePct in metrics", () => {
239
+ const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
240
+ const positions = [
241
+ { exchange: "a", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "60000") }, // 40%
242
+ { exchange: "b", position: makePosition("ETH", "short", 1, 3500, 5, 0, "4200") }, // 20%
243
+ ];
244
+ const result = assessRisk(balances, positions, defaultLimits);
245
+ expect(result.metrics.minLiquidationDistancePct).toBeCloseTo(20, 1);
246
+ });
247
+ it("should report -1 for minLiquidationDistancePct when no positions", () => {
248
+ const result = assessRisk([], [], defaultLimits);
249
+ expect(result.metrics.minLiquidationDistancePct).toBe(-1);
250
+ });
251
+ it("should include liquidationDistances array in assessment", () => {
252
+ const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
253
+ // BTC long at $100000, liq at $30000 → 70% distance (well above 30% * 1.5 = 45% → safe)
254
+ const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "30000") }];
255
+ const result = assessRisk(balances, positions, defaultLimits);
256
+ expect(result.liquidationDistances).toHaveLength(1);
257
+ expect(result.liquidationDistances[0].symbol).toBe("BTC");
258
+ expect(result.liquidationDistances[0].distancePct).toBeCloseTo(70, 1);
259
+ expect(result.liquidationDistances[0].status).toBe("safe");
260
+ });
261
+ });
262
+ describe("LIQUIDATION_DISTANCE_HARD_CAP", () => {
263
+ it("should be 20", () => {
264
+ expect(LIQUIDATION_DISTANCE_HARD_CAP).toBe(20);
265
+ });
266
+ });
@@ -1,6 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
3
- import { loadRiskLimits, saveRiskLimits, assessRisk } from "../risk.js";
3
+ import { loadRiskLimits, saveRiskLimits, assessRisk, getLiquidationDistances, LIQUIDATION_DISTANCE_HARD_CAP } from "../risk.js";
4
4
  const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
5
5
  export function registerRiskCommands(program, getAdapterForExchange, isJson) {
6
6
  const risk = program.command("risk").description("Risk management and guardrails");
@@ -56,6 +56,14 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
56
56
  console.log(` Positions: ${assessment.metrics.positionCount}`);
57
57
  console.log(` Largest Position: $${formatUsd(assessment.metrics.largestPositionUsd)}`);
58
58
  console.log(` Max Leverage Used: ${assessment.metrics.maxLeverageUsed}x`);
59
+ if (assessment.metrics.minLiquidationDistancePct >= 0) {
60
+ const ldColor = assessment.metrics.minLiquidationDistancePct < LIQUIDATION_DISTANCE_HARD_CAP
61
+ ? chalk.bgRed.white
62
+ : assessment.metrics.minLiquidationDistancePct < assessment.limits.minLiquidationDistance
63
+ ? chalk.red
64
+ : chalk.green;
65
+ console.log(` Min Liq Distance: ${ldColor(`${assessment.metrics.minLiquidationDistancePct.toFixed(1)}%`)}`);
66
+ }
59
67
  if (assessment.violations.length > 0) {
60
68
  console.log(chalk.red.bold("\n Violations"));
61
69
  const vRows = assessment.violations.map(v => {
@@ -89,15 +97,18 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
89
97
  .option("--max-positions <n>", "Max number of simultaneous positions")
90
98
  .option("--max-leverage <n>", "Max leverage per position")
91
99
  .option("--max-margin <pct>", "Max margin utilization %")
100
+ .option("--min-liq-distance <pct>", `Min liquidation distance % (hard cap: >=${LIQUIDATION_DISTANCE_HARD_CAP}%)`)
92
101
  .option("--reset", "Reset all limits to defaults")
93
102
  .action(async (opts) => {
94
103
  let limits = loadRiskLimits();
95
104
  const hasUpdate = opts.maxDrawdown || opts.maxPosition || opts.maxExposure ||
96
- opts.dailyLoss || opts.maxPositions || opts.maxLeverage || opts.maxMargin || opts.reset;
105
+ opts.dailyLoss || opts.maxPositions || opts.maxLeverage || opts.maxMargin ||
106
+ opts.minLiqDistance || opts.reset;
97
107
  if (opts.reset) {
98
108
  limits = {
99
109
  maxDrawdownUsd: 500, maxPositionUsd: 5000, maxTotalExposureUsd: 20000,
100
110
  dailyLossLimitUsd: 200, maxPositions: 10, maxLeverage: 20, maxMarginUtilization: 80,
111
+ minLiquidationDistance: 30,
101
112
  };
102
113
  }
103
114
  if (opts.maxDrawdown)
@@ -114,6 +125,17 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
114
125
  limits.maxLeverage = parseInt(opts.maxLeverage);
115
126
  if (opts.maxMargin)
116
127
  limits.maxMarginUtilization = parseFloat(opts.maxMargin);
128
+ if (opts.minLiqDistance) {
129
+ const val = parseFloat(opts.minLiqDistance);
130
+ if (val < LIQUIDATION_DISTANCE_HARD_CAP) {
131
+ const msg = `Cannot set min liquidation distance below ${LIQUIDATION_DISTANCE_HARD_CAP}% (hard cap). Got: ${val}%`;
132
+ if (isJson())
133
+ return printJson(jsonOk({ error: msg, hardCap: LIQUIDATION_DISTANCE_HARD_CAP }));
134
+ console.log(chalk.red(`\n ${msg}\n`));
135
+ return;
136
+ }
137
+ limits.minLiquidationDistance = val;
138
+ }
117
139
  if (hasUpdate)
118
140
  saveRiskLimits(limits);
119
141
  if (isJson())
@@ -125,7 +147,8 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
125
147
  console.log(` Daily Loss Limit: $${formatUsd(limits.dailyLossLimitUsd)}`);
126
148
  console.log(` Max Positions: ${limits.maxPositions}`);
127
149
  console.log(` Max Leverage: ${limits.maxLeverage}x`);
128
- console.log(` Max Margin Util: ${limits.maxMarginUtilization}%\n`);
150
+ console.log(` Max Margin Util: ${limits.maxMarginUtilization}%`);
151
+ console.log(` Min Liq Distance: ${limits.minLiquidationDistance}% ${chalk.gray(`(hard cap: ${LIQUIDATION_DISTANCE_HARD_CAP}%)`)}\n`);
129
152
  console.log(chalk.gray(` Config file: ~/.perp/risk.json\n`));
130
153
  });
131
154
  // ── risk check ── (pre-trade check, for agent use)
@@ -166,4 +189,84 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
166
189
  }
167
190
  });
168
191
  });
192
+ // ── risk liquidation-distance ──
193
+ risk
194
+ .command("liquidation-distance")
195
+ .description("Show % distance from liquidation price for all positions")
196
+ .alias("liq-dist")
197
+ .option("--exchange <exchanges>", "Comma-separated exchanges (default: all)")
198
+ .action(async (opts) => {
199
+ await withJsonErrors(isJson(), async () => {
200
+ const exchanges = opts.exchange
201
+ ? opts.exchange.split(",").map(e => e.trim())
202
+ : [...EXCHANGES];
203
+ const positions = [];
204
+ const results = await Promise.allSettled(exchanges.map(async (ex) => {
205
+ const adapter = await getAdapterForExchange(ex);
206
+ const pos = await adapter.getPositions();
207
+ for (const p of pos)
208
+ positions.push({ exchange: ex, position: p });
209
+ }));
210
+ for (let i = 0; i < results.length; i++) {
211
+ if (results[i].status === "rejected") {
212
+ const err = results[i].reason;
213
+ if (!isJson()) {
214
+ console.log(chalk.yellow(` ${exchanges[i]}: ${err instanceof Error ? err.message : String(err)}`));
215
+ }
216
+ }
217
+ }
218
+ if (positions.length === 0) {
219
+ if (isJson())
220
+ return printJson(jsonOk({ positions: [], message: "No open positions" }));
221
+ console.log(chalk.gray("\n No open positions.\n"));
222
+ return;
223
+ }
224
+ const limits = loadRiskLimits();
225
+ const distances = getLiquidationDistances(positions, limits);
226
+ if (isJson()) {
227
+ return printJson(jsonOk({
228
+ positions: distances,
229
+ limits: {
230
+ minLiquidationDistance: limits.minLiquidationDistance,
231
+ hardCap: LIQUIDATION_DISTANCE_HARD_CAP,
232
+ },
233
+ }));
234
+ }
235
+ console.log(chalk.cyan.bold("\n Liquidation Distance Report\n"));
236
+ console.log(chalk.gray(` Your limit: ${limits.minLiquidationDistance}% | Hard cap: ${LIQUIDATION_DISTANCE_HARD_CAP}%\n`));
237
+ const rows = distances.map(d => {
238
+ const statusColor = {
239
+ safe: chalk.green,
240
+ warning: chalk.yellow,
241
+ danger: chalk.red,
242
+ critical: chalk.bgRed.white,
243
+ }[d.status];
244
+ return [
245
+ chalk.white(d.exchange),
246
+ chalk.white.bold(d.symbol),
247
+ d.side === "long" ? chalk.green("LONG") : chalk.red("SHORT"),
248
+ `$${formatUsd(d.markPrice)}`,
249
+ `$${formatUsd(d.liquidationPrice)}`,
250
+ statusColor(`${d.distancePct.toFixed(1)}%`),
251
+ statusColor(d.status.toUpperCase()),
252
+ ];
253
+ });
254
+ console.log(makeTable(["Exchange", "Symbol", "Side", "Mark Price", "Liq Price", "Distance", "Status"], rows));
255
+ // Summary warnings
256
+ const critical = distances.filter(d => d.status === "critical");
257
+ const danger = distances.filter(d => d.status === "danger");
258
+ if (critical.length > 0) {
259
+ console.log(chalk.bgRed.white.bold(` ⚠ ${critical.length} position(s) BELOW HARD CAP (${LIQUIDATION_DISTANCE_HARD_CAP}%) — REDUCE IMMEDIATELY`));
260
+ }
261
+ if (danger.length > 0) {
262
+ console.log(chalk.red.bold(` ⚠ ${danger.length} position(s) below your limit (${limits.minLiquidationDistance}%) — action recommended`));
263
+ }
264
+ if (critical.length === 0 && danger.length === 0) {
265
+ console.log(chalk.green(" All positions within safe liquidation distance.\n"));
266
+ }
267
+ else {
268
+ console.log();
269
+ }
270
+ });
271
+ });
169
272
  }
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ const _defaultExchange = _settings.defaultExchange || "pacifica";
48
48
  program
49
49
  .name("perp")
50
50
  .description("Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter)")
51
- .version("0.3.3")
51
+ .version("0.3.4")
52
52
  .option("-e, --exchange <exchange>", `Exchange: pacifica, hyperliquid, lighter (default: ${_defaultExchange})`, _defaultExchange)
53
53
  .option("-n, --network <network>", "Network: mainnet or testnet", "mainnet")
54
54
  .option("-k, --private-key <key>", "Private key")
@@ -383,7 +383,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
383
383
  process.env.LIGHTER_PRIVATE_KEY);
384
384
  if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
385
385
  // Fresh install — onboarding
386
- console.log(chalk.cyan.bold("\n Welcome to perp-cli!") + chalk.gray(" v0.3.3\n"));
386
+ console.log(chalk.cyan.bold("\n Welcome to perp-cli!") + chalk.gray(" v0.3.4\n"));
387
387
  console.log(" Multi-DEX perpetual futures CLI for Pacifica, Hyperliquid, and Lighter.\n");
388
388
  console.log(` Get started: ${chalk.cyan("perp init")}`);
389
389
  console.log(chalk.gray(`\n Or explore without a wallet:`));
@@ -396,7 +396,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
396
396
  // Configured — show status overview
397
397
  const defaultEx = settings.defaultExchange || "pacifica";
398
398
  const activeEntries = Object.entries(status.active);
399
- console.log(chalk.cyan.bold("\n perp-cli") + chalk.gray(" v0.3.3\n"));
399
+ console.log(chalk.cyan.bold("\n perp-cli") + chalk.gray(" v0.3.4\n"));
400
400
  console.log(` Default exchange: ${chalk.cyan(defaultEx)}`);
401
401
  if (activeEntries.length > 0) {
402
402
  console.log(chalk.white.bold("\n Wallets:"));
@@ -56,7 +56,7 @@ function err(error, meta) {
56
56
  return JSON.stringify({ ok: false, error, meta }, null, 2);
57
57
  }
58
58
  // ── MCP Server ──
59
- const server = new McpServer({ name: "perp-cli", version: "0.3.3" }, { capabilities: { tools: {}, resources: {} } });
59
+ const server = new McpServer({ name: "perp-cli", version: "0.3.4" }, { capabilities: { tools: {}, resources: {} } });
60
60
  // ============================================================
61
61
  // Market Data tools (read-only, no private key needed)
62
62
  // ============================================================
package/dist/risk.d.ts CHANGED
@@ -7,7 +7,10 @@ export interface RiskLimits {
7
7
  maxPositions: number;
8
8
  maxLeverage: number;
9
9
  maxMarginUtilization: number;
10
+ minLiquidationDistance: number;
10
11
  }
12
+ /** Hard cap: liquidation distance can NEVER be set below this % */
13
+ export declare const LIQUIDATION_DISTANCE_HARD_CAP = 20;
11
14
  export declare function loadRiskLimits(): RiskLimits;
12
15
  export declare function saveRiskLimits(limits: RiskLimits): void;
13
16
  export type RiskLevel = "low" | "medium" | "high" | "critical";
@@ -18,6 +21,21 @@ export interface RiskViolation {
18
21
  current: number;
19
22
  limit: number;
20
23
  }
24
+ export interface LiquidationDistanceInfo {
25
+ exchange: string;
26
+ symbol: string;
27
+ side: "long" | "short";
28
+ markPrice: number;
29
+ liquidationPrice: number;
30
+ distancePct: number;
31
+ status: "safe" | "warning" | "danger" | "critical";
32
+ }
33
+ /** Calculate % distance from current price to liquidation price */
34
+ export declare function calcLiquidationDistance(markPrice: number, liquidationPrice: number, side: "long" | "short"): number;
35
+ export declare function getLiquidationDistances(positions: {
36
+ exchange: string;
37
+ position: ExchangePosition;
38
+ }[], limits?: RiskLimits): LiquidationDistanceInfo[];
21
39
  export interface RiskAssessment {
22
40
  level: RiskLevel;
23
41
  violations: RiskViolation[];
@@ -30,7 +48,9 @@ export interface RiskAssessment {
30
48
  marginUtilization: number;
31
49
  largestPositionUsd: number;
32
50
  maxLeverageUsed: number;
51
+ minLiquidationDistancePct: number;
33
52
  };
53
+ liquidationDistances: LiquidationDistanceInfo[];
34
54
  limits: RiskLimits;
35
55
  canTrade: boolean;
36
56
  }
package/dist/risk.js CHANGED
@@ -8,7 +8,10 @@ const DEFAULT_LIMITS = {
8
8
  maxPositions: 10,
9
9
  maxLeverage: 20,
10
10
  maxMarginUtilization: 80,
11
+ minLiquidationDistance: 30,
11
12
  };
13
+ /** Hard cap: liquidation distance can NEVER be set below this % */
14
+ export const LIQUIDATION_DISTANCE_HARD_CAP = 20;
12
15
  const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
13
16
  const RISK_FILE = resolve(PERP_DIR, "risk.json");
14
17
  export function loadRiskLimits() {
@@ -27,6 +30,38 @@ export function saveRiskLimits(limits) {
27
30
  mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
28
31
  writeFileSync(RISK_FILE, JSON.stringify(limits, null, 2), { mode: 0o600 });
29
32
  }
33
+ /** Calculate % distance from current price to liquidation price */
34
+ export function calcLiquidationDistance(markPrice, liquidationPrice, side) {
35
+ if (liquidationPrice <= 0 || markPrice <= 0)
36
+ return Infinity;
37
+ if (side === "long") {
38
+ // Long: liq price is below mark price
39
+ return ((markPrice - liquidationPrice) / markPrice) * 100;
40
+ }
41
+ else {
42
+ // Short: liq price is above mark price
43
+ return ((liquidationPrice - markPrice) / markPrice) * 100;
44
+ }
45
+ }
46
+ export function getLiquidationDistances(positions, limits) {
47
+ const lim = limits ?? loadRiskLimits();
48
+ return positions
49
+ .filter(({ position: p }) => p.liquidationPrice !== "N/A" && Number(p.liquidationPrice) > 0)
50
+ .map(({ exchange, position: p }) => {
51
+ const markPrice = Number(p.markPrice);
52
+ const liquidationPrice = Number(p.liquidationPrice);
53
+ const distancePct = calcLiquidationDistance(markPrice, liquidationPrice, p.side);
54
+ let status = "safe";
55
+ if (distancePct < LIQUIDATION_DISTANCE_HARD_CAP)
56
+ status = "critical";
57
+ else if (distancePct < lim.minLiquidationDistance)
58
+ status = "danger";
59
+ else if (distancePct < lim.minLiquidationDistance * 1.5)
60
+ status = "warning";
61
+ return { exchange, symbol: p.symbol, side: p.side, markPrice, liquidationPrice, distancePct, status };
62
+ })
63
+ .sort((a, b) => a.distancePct - b.distancePct);
64
+ }
30
65
  export function assessRisk(balances, positions, limits) {
31
66
  const lim = limits ?? loadRiskLimits();
32
67
  const violations = [];
@@ -107,6 +142,34 @@ export function assessRisk(balances, positions, limits) {
107
142
  limit: lim.maxMarginUtilization,
108
143
  });
109
144
  }
145
+ // Check liquidation distances
146
+ const liquidationDistances = getLiquidationDistances(positions, lim);
147
+ let minLiquidationDistancePct = Infinity;
148
+ for (const ld of liquidationDistances) {
149
+ if (ld.distancePct < minLiquidationDistancePct) {
150
+ minLiquidationDistancePct = ld.distancePct;
151
+ }
152
+ if (ld.distancePct < LIQUIDATION_DISTANCE_HARD_CAP) {
153
+ violations.push({
154
+ rule: "liquidation_distance_hard_cap",
155
+ severity: "critical",
156
+ message: `${ld.exchange}:${ld.symbol} is ${ld.distancePct.toFixed(1)}% from liquidation (hard cap: ${LIQUIDATION_DISTANCE_HARD_CAP}%)`,
157
+ current: ld.distancePct,
158
+ limit: LIQUIDATION_DISTANCE_HARD_CAP,
159
+ });
160
+ }
161
+ else if (ld.distancePct < lim.minLiquidationDistance) {
162
+ violations.push({
163
+ rule: "min_liquidation_distance",
164
+ severity: "high",
165
+ message: `${ld.exchange}:${ld.symbol} is ${ld.distancePct.toFixed(1)}% from liquidation (limit: ${lim.minLiquidationDistance}%)`,
166
+ current: ld.distancePct,
167
+ limit: lim.minLiquidationDistance,
168
+ });
169
+ }
170
+ }
171
+ if (minLiquidationDistancePct === Infinity)
172
+ minLiquidationDistancePct = -1; // no positions
110
173
  // Determine overall risk level
111
174
  let level = "low";
112
175
  if (violations.some(v => v.severity === "critical"))
@@ -129,7 +192,9 @@ export function assessRisk(balances, positions, limits) {
129
192
  marginUtilization,
130
193
  largestPositionUsd,
131
194
  maxLeverageUsed,
195
+ minLiquidationDistancePct,
132
196
  },
197
+ liquidationDistances,
133
198
  limits: lim,
134
199
  canTrade,
135
200
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "./dist/index.js",
@@ -1,10 +1,11 @@
1
1
  ---
2
2
  name: perp-cli
3
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. Also use when user says 'set up perp trading', 'check my positions', 'buy BTC perps', 'funding rate arb', 'bridge USDC', or 'deposit to exchange'."
4
+ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
4
5
  license: MIT
5
6
  metadata:
6
7
  author: hypurrquant
7
- version: "0.3.3"
8
+ version: "0.3.4"
8
9
  mcp-server: perp-cli
9
10
  ---
10
11
 
@@ -14,10 +15,13 @@ Multi-DEX perpetual futures CLI — Pacifica (Solana), Hyperliquid (HyperEVM), L
14
15
 
15
16
  ## Critical Rules
16
17
 
17
- 1. **NEVER use interactive commands.** Do NOT run `perp init`. Always use non-interactive commands with `--json`.
18
- 2. **Always use `--json`** on every command for structured output.
19
- 3. **NEVER trade without user confirmation.** Show order details and wait for explicit approval.
20
- 4. **Verify wallet before any operation.** Run `perp --json wallet show` first.
18
+ 1. **RISK MANAGEMENT IS YOUR #1 PRIORITY.** A single liquidation wipes out months of profit. Always check `perp --json risk status` before and during any operation. See `references/strategies.md` for the full risk framework.
19
+ 2. **NEVER use interactive commands.** Do NOT run `perp init`. Always use non-interactive commands with `--json`.
20
+ 3. **Always use `--json`** on every command for structured output.
21
+ 4. **NEVER trade without user confirmation.** Show order details and wait for explicit approval.
22
+ 5. **Verify wallet before any operation.** Run `perp --json wallet show` first.
23
+ 6. **Use ISOLATED margin for arb.** Set `perp --json manage margin <SYM> isolated` before opening positions. Cross margin can cascade liquidations.
24
+ 7. **Monitor positions continuously.** Run `perp --json risk status` and `perp --json -e <EX> account positions` every 15 minutes while positions are open.
21
25
 
22
26
  ## Step 1: Install
23
27
 
@@ -74,15 +78,19 @@ perp --json portfolio # unified multi-exchange view
74
78
 
75
79
  ### Trade execution (MANDATORY checklist)
76
80
  ```
77
- 1. perp --json -e <EX> account info → verify balance
78
- 2. perp --json -e <EX> market mid <SYM> current price
79
- 3. perp --json -e <EX> trade check <SYM> <SIDE> <SIZE> pre-flight validation
80
- 4. [Show order details to user, get explicit confirmation]
81
- 5. perp --json -e <EX> trade market <SYM> <SIDE> <SIZE> execute
82
- 6. perp --json -e <EX> account positions → verify result
81
+ 1. perp --json risk status → check risk level (STOP if critical)
82
+ 2. perp --json -e <EX> account info verify balance
83
+ 3. perp --json -e <EX> market mid <SYM> current price
84
+ 4. perp --json risk check --notional <$> --leverage <L> → risk pre-check
85
+ 5. perp --json -e <EX> trade check <SYM> <SIDE> <SIZE> trade validation
86
+ 6. [Show order details + risk assessment to user, get explicit confirmation]
87
+ 7. perp --json -e <EX> trade market <SYM> <SIDE> <SIZE> → execute
88
+ 8. perp --json -e <EX> account positions → verify result + check liquidation price
83
89
  ```
84
90
 
85
91
  For full command reference, see `references/commands.md`.
92
+ For agent-specific operations (setup flows, deposit/withdraw, order types, idempotency), see `references/agent-operations.md`.
93
+ For autonomous strategies (funding rate arb, risk management, opportunity cost), see `references/strategies.md`.
86
94
 
87
95
  ## Response Format
88
96
 
@@ -0,0 +1,243 @@
1
+ # Agent Operations Guide
2
+
3
+ Complete reference for non-interactive CLI operations. Every command here is safe for agents — no prompts, no hangs.
4
+
5
+ ## Interactive vs Non-Interactive Commands
6
+
7
+ ### NEVER use these (interactive, will hang):
8
+ ```
9
+ perp init # interactive wizard — asks questions via stdin
10
+ perp wallet setup # removed, but may appear in old docs
11
+ ```
12
+
13
+ ### ALWAYS use these (non-interactive, agent-safe):
14
+ ```bash
15
+ perp --json wallet set <exchange> <key> # set private key
16
+ perp --json wallet generate evm # generate EVM wallet
17
+ perp --json wallet generate solana # generate Solana wallet
18
+ perp --json wallet show # check configured wallets
19
+ perp --json wallet balance # on-chain USDC balances
20
+ perp --json -e <EX> account info # exchange account info
21
+ perp --json -e <EX> account positions # open positions
22
+ perp --json -e <EX> market list # available markets
23
+ perp --json -e <EX> trade market ... # execute trade
24
+ perp --json -e <EX> trade buy ... # alias for market buy
25
+ perp --json -e <EX> trade sell ... # alias for market sell
26
+ perp --json risk status # risk assessment
27
+ perp --json risk liquidation-distance # % from liquidation for all positions
28
+ perp --json risk limits # view/set risk limits
29
+ perp --json risk check --notional <$> --leverage <L> # pre-trade risk check
30
+ ```
31
+
32
+ **Rule: every command MUST include `--json`.** Without it, output is human-formatted and harder to parse.
33
+
34
+ ## Zero to Trading: Complete Setup Flow
35
+
36
+ ### Single Exchange Setup
37
+ ```bash
38
+ # 1. Install
39
+ npm install -g perp-cli
40
+
41
+ # 2. Register wallet (user provides key)
42
+ perp --json wallet set hl 0xUSER_PRIVATE_KEY
43
+
44
+ # 3. Verify
45
+ perp --json wallet show
46
+ # → check "ok": true and address appears
47
+
48
+ # 4. Check balance
49
+ perp --json -e hl account info
50
+ # → if balance is 0, tell user to deposit USDC
51
+
52
+ # 5. Ready to trade
53
+ perp --json -e hl market list
54
+ ```
55
+
56
+ ### Multi-Exchange Setup (for Funding Rate Arb)
57
+ To run arb, you need wallets on AT LEAST 2 exchanges. Each exchange needs:
58
+ - A configured wallet with a private key
59
+ - USDC balance deposited on-exchange
60
+
61
+ ```bash
62
+ # 1. Register both wallets
63
+ perp --json wallet set hl 0xEVM_KEY
64
+ perp --json wallet set pac SOLANA_BASE58_KEY
65
+
66
+ # 2. Verify both
67
+ perp --json wallet show
68
+ # → should show both exchanges with addresses
69
+
70
+ # 3. Check balances on both
71
+ perp --json -e hl account info
72
+ perp --json -e pac account info
73
+
74
+ # 4. If one side needs funding, bridge USDC
75
+ perp --json bridge quote --from solana --to arbitrum --amount 500
76
+ # → show quote to user, get confirmation
77
+ perp --json bridge send --from solana --to arbitrum --amount 500
78
+ perp --json bridge status <orderId> # wait for completion
79
+
80
+ # 5. Verify both sides have balance, then start arb
81
+ perp --json arb rates
82
+ ```
83
+
84
+ ### Using the Same EVM Key for Multiple Exchanges
85
+ One EVM private key works for both Hyperliquid and Lighter:
86
+ ```bash
87
+ perp --json wallet set hl 0xKEY
88
+ perp --json wallet set lt 0xKEY # same key, different exchange binding
89
+ ```
90
+
91
+ ## Wallet Key Types
92
+
93
+ | Exchange | Chain | Key Format | Example |
94
+ |----------|-------|-----------|---------|
95
+ | Hyperliquid | EVM | Hex with 0x prefix, 66 chars | `0x4c0883a69102937d...` |
96
+ | Lighter | EVM | Hex with 0x prefix, 66 chars | `0x4c0883a69102937d...` |
97
+ | Pacifica | Solana | Base58 string | `5KQwrPbwdL6PhXu...` |
98
+
99
+ Aliases for exchange names:
100
+ - `hl` or `hyperliquid`
101
+ - `pac` or `pacifica`
102
+ - `lt` or `lighter`
103
+
104
+ ## Deposit & Withdraw Flows
105
+
106
+ ### Check On-Chain vs Exchange Balance
107
+ ```bash
108
+ perp --json wallet balance # on-chain USDC in your wallet
109
+ perp --json -e hl account info # USDC deposited on exchange
110
+ ```
111
+
112
+ **On-chain balance ≠ exchange balance.** USDC in your wallet must be deposited to the exchange before trading.
113
+
114
+ ### Deposit to Exchange
115
+ ```bash
116
+ # Hyperliquid (from Arbitrum wallet)
117
+ perp --json deposit hyperliquid 100
118
+
119
+ # Pacifica (from Solana wallet)
120
+ perp --json deposit pacifica 100
121
+
122
+ # Lighter (multiple routes)
123
+ perp --json deposit lighter info # show all available routes
124
+ perp --json deposit lighter cctp arb 100 # via CCTP from Arbitrum
125
+ ```
126
+
127
+ ### Withdraw from Exchange
128
+ ```bash
129
+ perp --json withdraw hyperliquid 100
130
+ perp --json withdraw pacifica 100
131
+ ```
132
+
133
+ ### Bridge Between Chains
134
+ When you need to move USDC between exchanges on different chains:
135
+ ```bash
136
+ # 1. Withdraw from source exchange
137
+ perp --json withdraw pacifica 500
138
+
139
+ # 2. Quote the bridge
140
+ perp --json bridge quote --from solana --to arbitrum --amount 500
141
+
142
+ # 3. Send (after user confirmation)
143
+ perp --json bridge send --from solana --to arbitrum --amount 500
144
+
145
+ # 4. Wait for completion
146
+ perp --json bridge status <orderId>
147
+
148
+ # 5. Deposit to destination exchange
149
+ perp --json deposit hyperliquid 500
150
+ ```
151
+
152
+ ## Order Types & Execution
153
+
154
+ ### Market Orders (immediate execution)
155
+ ```bash
156
+ perp --json -e hl trade market BTC buy 0.01 # market buy
157
+ perp --json -e hl trade market BTC sell 0.01 # market sell
158
+ perp --json -e hl trade buy BTC 0.01 # shorthand
159
+ perp --json -e hl trade sell BTC 0.01 # shorthand
160
+ ```
161
+
162
+ ### Limit Orders (execute at specific price)
163
+ ```bash
164
+ perp --json -e hl trade buy BTC 0.01 -p 60000 # buy at $60,000
165
+ perp --json -e hl trade sell BTC 0.01 -p 70000 # sell at $70,000
166
+ ```
167
+
168
+ ### Close Position
169
+ ```bash
170
+ perp --json -e hl trade close BTC # close entire position
171
+ ```
172
+
173
+ ### Stop Loss / Take Profit
174
+ ```bash
175
+ perp --json -e hl trade sl BTC 58000 # stop loss at $58k
176
+ perp --json -e hl trade tp BTC 65000 # take profit at $65k
177
+ ```
178
+
179
+ ### Cancel Orders
180
+ ```bash
181
+ perp --json -e hl account orders # list open orders
182
+ perp --json -e hl trade cancel <orderId> # cancel specific order
183
+ ```
184
+
185
+ ### Pre-Flight Validation
186
+ ALWAYS run before executing a trade:
187
+ ```bash
188
+ perp --json -e hl trade check BTC buy 0.01
189
+ ```
190
+ This returns estimated fees, slippage, and whether the trade can execute.
191
+
192
+ ## Parsing JSON Output
193
+
194
+ Every command returns this envelope:
195
+ ```json
196
+ {
197
+ "ok": true,
198
+ "data": { ... },
199
+ "meta": { "timestamp": "2026-03-11T..." }
200
+ }
201
+ ```
202
+
203
+ Error case:
204
+ ```json
205
+ {
206
+ "ok": false,
207
+ "error": {
208
+ "code": "INSUFFICIENT_BALANCE",
209
+ "message": "Not enough USDC",
210
+ "retryable": false
211
+ }
212
+ }
213
+ ```
214
+
215
+ **Always check `ok` field first.** If `ok` is `false`, read `error.code` to decide next action. If `error.retryable` is `true`, wait 5 seconds and retry once.
216
+
217
+ ## Idempotency & Safety
218
+
219
+ ### Safe to retry (idempotent):
220
+ - `wallet show`, `wallet balance` — read-only
221
+ - `account info`, `account positions`, `account orders` — read-only
222
+ - `market list`, `market mid`, `market book` — read-only
223
+ - `arb rates`, `arb scan` — read-only
224
+ - `portfolio`, `risk overview` — read-only
225
+ - `bridge quote` — read-only
226
+ - `bridge status` — read-only
227
+
228
+ ### NOT safe to retry blindly:
229
+ - `trade market`, `trade buy`, `trade sell` — will open duplicate positions
230
+ - `trade close` — may error if already closed, but harmless
231
+ - `bridge send` — will send duplicate transfers
232
+ - `deposit`, `withdraw` — will move funds twice
233
+
234
+ **For non-idempotent commands:** always verify the result before retrying. Check positions or balances to confirm whether the first attempt succeeded.
235
+
236
+ ## Common Agent Mistakes
237
+
238
+ 1. **Using `perp init`** — interactive, will hang forever. Use `wallet set` instead.
239
+ 2. **Forgetting `--json`** — output becomes unparseable human text.
240
+ 3. **Trading with zero balance** — check `account info` first, tell user to deposit.
241
+ 4. **Retrying a trade without checking** — leads to double positions. Always check `account positions` after a trade, even if it seemed to fail.
242
+ 5. **Bridging without quoting** — always run `bridge quote` first to show the user fees and estimated time.
243
+ 6. **Assuming deposit is instant** — after `bridge send`, wait for `bridge status` to confirm completion before depositing to the destination exchange.
@@ -108,8 +108,11 @@ perp --json wallet balance # on-chain balance
108
108
 
109
109
  ## Risk & Analytics
110
110
  ```bash
111
- perp --json risk status # portfolio risk overview
112
- perp --json risk limits # position limits
111
+ perp --json risk status # portfolio risk overview (level, violations, canTrade)
112
+ perp --json risk liquidation-distance # % distance from liquidation for ALL positions
113
+ perp --json risk limits # view current risk limits
114
+ perp --json risk limits --min-liq-distance 30 --max-leverage 5 # set risk limits
115
+ perp --json risk check --notional 1000 --leverage 3 # pre-trade risk check
113
116
  perp --json health # exchange connectivity & latency
114
117
  perp --json analytics summary # trading performance
115
118
  perp --json analytics pnl # P&L breakdown
@@ -0,0 +1,343 @@
1
+ # Autonomous Strategy Framework
2
+
3
+ This guide helps agents make informed decisions about perpetual futures trading across exchanges.
4
+ You are not expected to follow rigid rules — use this as a decision framework and develop your own judgment over time.
5
+
6
+ ## Core Concepts
7
+
8
+ ### Funding Rate Mechanics
9
+ - Funding rates settle **every 1 hour** on all supported exchanges
10
+ - Positive rate = longs pay shorts, negative rate = shorts pay longs
11
+ - Rates are annualized in display but applied hourly: `hourly = annual / 8760`
12
+ - Scan rates: `perp --json arb rates`
13
+
14
+ ### Opportunity Cost Awareness
15
+
16
+ **This is critical.** Before switching any position, calculate the FULL cost of the switch:
17
+
18
+ ```
19
+ Switch Cost =
20
+ + close current position (trading fee + slippage)
21
+ + withdraw from exchange (gas fee + time)
22
+ + bridge to target chain (bridge fee + gas + time)
23
+ + deposit to new exchange (gas fee + time)
24
+ + open new position (trading fee + slippage)
25
+ ```
26
+
27
+ Query each component:
28
+ ```bash
29
+ perp --json -e <FROM> trade check <SYM> <SIDE> <SIZE> # close cost estimate
30
+ perp --json bridge quote --from <CHAIN> --to <CHAIN> --amount <AMT> # bridge cost
31
+ perp --json -e <TO> trade check <SYM> <SIDE> <SIZE> # open cost estimate
32
+ ```
33
+
34
+ **Only switch if:**
35
+ ```
36
+ expected_hourly_gain × expected_hours_held > total_switch_cost × safety_margin
37
+ ```
38
+
39
+ Where `safety_margin` should be at least 2x — rates can change before you finish switching.
40
+
41
+ ### Time Cost
42
+ Switching is not instant. Estimate total transition time:
43
+ - Exchange withdrawal: 1-30 minutes
44
+ - Bridge transfer: 1-20 minutes (CCTP ~2-5 min, deBridge ~5-15 min)
45
+ - Exchange deposit confirmation: 1-10 minutes
46
+
47
+ During transition, you are **unhedged**. Price can move against you. Factor this risk in.
48
+
49
+ ## Funding Rate Arbitrage
50
+
51
+ ### How It Works
52
+ 1. Find a symbol where Exchange A pays significantly more funding than Exchange B
53
+ 2. Go short on Exchange A (receive funding) and long on Exchange B (pay less funding)
54
+ 3. Net = funding received - funding paid - fees
55
+ 4. The position is market-neutral (delta-hedged) — you profit from the rate spread
56
+
57
+ ### Discovery Loop
58
+ ```bash
59
+ perp --json arb rates # compare rates across exchanges
60
+ perp --json arb scan --min 10 # find spreads > 10 bps
61
+ ```
62
+
63
+ ### Decision Framework
64
+ When evaluating an arb opportunity:
65
+
66
+ 1. **Is the spread real?** Check if rates are stable or spiking temporarily
67
+ - Query rates multiple times over 15-30 minutes
68
+ - A spike that reverts in 1 hour is not worth switching for
69
+
70
+ 2. **What's my current position earning?**
71
+ - If already in a profitable arb, switching has opportunity cost
72
+ - Calculate: `current_hourly_income vs new_hourly_income - switch_cost`
73
+
74
+ 3. **How long will the spread persist?**
75
+ - Historical funding tends to mean-revert
76
+ - Higher confidence in moderate, stable spreads (20-50 bps) than extreme spikes (>100 bps)
77
+
78
+ 4. **Can I execute both legs atomically?**
79
+ - Both positions should open near-simultaneously to minimize directional exposure
80
+ - If capital needs to bridge first, you're exposed during transit
81
+
82
+ ### Monitoring Active Positions
83
+ ```bash
84
+ perp --json portfolio # unified multi-exchange view
85
+ perp --json risk overview # cross-exchange risk assessment
86
+ perp --json -e <EX> account positions # per-exchange positions
87
+ perp --json arb rates # are current rates still favorable?
88
+ ```
89
+
90
+ ### Order Execution: Sequential Leg Management
91
+
92
+ **NEVER close or open both legs of an arb at once with market orders.** You must manage execution carefully.
93
+
94
+ #### Why This Matters
95
+ Orderbooks have limited depth at each price level. A large market order will eat through multiple ticks and suffer heavy slippage. Worse, if you close one leg but fail to close the other (exchange error, rate limit, network issue), you are left with naked directional exposure.
96
+
97
+ #### Pre-Execution: Check Orderbook Depth
98
+ Before executing, verify that the orderbook can absorb your size at acceptable prices on BOTH sides:
99
+ ```bash
100
+ perp --json -e <EX_A> market book <SYM> # check bids/asks depth
101
+ perp --json -e <EX_B> market book <SYM> # check bids/asks depth
102
+ ```
103
+
104
+ Look at the size available at the best tick. If your order size exceeds what's available at the best 2-3 ticks, you MUST split the order.
105
+
106
+ #### Execution Strategy
107
+ 1. **Determine executable chunk size** — the largest size both orderbooks can absorb at the best tick without excessive slippage
108
+ 2. **Execute in sequential chunks:**
109
+ ```
110
+ Chunk 1: close X on Exchange A → immediately open X on Exchange B
111
+ Chunk 2: close X on Exchange A → immediately open X on Exchange B
112
+ ... repeat until full size is executed
113
+ ```
114
+ 3. **Verify each chunk** before proceeding to the next:
115
+ ```bash
116
+ perp --json -e <EX_A> account positions # confirm partial close
117
+ perp --json -e <EX_B> account positions # confirm partial open
118
+ ```
119
+ 4. **Re-check the orderbook** between chunks — liquidity may have changed
120
+
121
+ #### Paired Execution Rule
122
+ Each chunk must be a **matched pair**: close on one side, open on the other. Never execute multiple closes without the corresponding opens. If one leg fails:
123
+ - STOP immediately
124
+ - Assess your current exposure
125
+ - Decide whether to retry the failed leg or unwind the completed leg
126
+ - Do NOT continue with remaining chunks
127
+
128
+ #### Using Limit Orders for Better Execution
129
+ For non-urgent transitions, consider limit orders at the best bid/ask instead of market orders:
130
+ ```bash
131
+ perp --json -e <EX> trade sell <SYM> <SIZE> -p <PRICE> # limit order
132
+ ```
133
+ This avoids crossing the spread, but you risk not getting filled. Set a reasonable timeout and fall back to market if not filled.
134
+
135
+ ### When to Exit
136
+ - Spread compressed below your breakeven (including fees)
137
+ - One side's funding flipped direction
138
+ - Better opportunity found (after accounting for switch cost!)
139
+ - Risk limits breached
140
+
141
+ ## Risk Management — THE MOST IMPORTANT SECTION
142
+
143
+ **Risk management is more important than profit.** A single liquidation can wipe out months of funding rate income. Every decision you make should be filtered through risk first, profit second.
144
+
145
+ ### Understanding Liquidation
146
+
147
+ When your margin drops below the maintenance requirement, the exchange force-closes your position at a loss. This is **liquidation** — it is catastrophic and must be avoided at all costs.
148
+
149
+ **Check liquidation distance for every open position:**
150
+ ```bash
151
+ perp --json risk liquidation-distance # shows % distance from liq price for ALL positions
152
+ perp --json -e <EX> account positions # shows liquidationPrice per position
153
+ perp --json -e <EX> account margin <SYM> # detailed: liquidationPrice, marginRequired, marginPctOfEquity
154
+ ```
155
+
156
+ **Liquidation distance is configurable by the user.** You MUST ask the user what risk tolerance they want:
157
+ ```bash
158
+ # Ask user: "What minimum liquidation distance do you want? (default: 30%, hard minimum: 20%)"
159
+ perp --json risk limits --min-liq-distance <USER_CHOICE>
160
+ ```
161
+
162
+ **Hard cap: 20%.** No matter what the user says, the system will NEVER allow a position to get within 20% of liquidation. This is non-negotiable and enforced at the system level. If a user tries to set it below 20%, the command will reject it.
163
+
164
+ **Action rules based on `risk liquidation-distance` output:**
165
+ - `status: "safe"` → no action needed
166
+ - `status: "warning"` → monitor more frequently (every 5 minutes)
167
+ - `status: "danger"` → alert user, recommend reducing position size
168
+ - `status: "critical"` (below 20% hard cap) → REDUCE IMMEDIATELY, `canTrade` becomes `false`
169
+
170
+ ### Leverage and Margin Mode
171
+
172
+ #### Leverage
173
+ Higher leverage = closer liquidation price = higher risk. For funding rate arb:
174
+ - **Recommended: 1x-3x leverage.** Arb profits are small but consistent — no need to amplify risk.
175
+ - **NEVER exceed 5x for arb positions.** The goal is to collect funding, not to speculate.
176
+ - For directional trades (non-arb), leverage should be set according to user's risk tolerance, but always confirm with user.
177
+
178
+ **Set leverage BEFORE opening a position:**
179
+ ```bash
180
+ perp --json -e <EX> trade leverage <SYM> <LEVERAGE>
181
+ # Example: perp --json -e hl trade leverage BTC 2
182
+ ```
183
+
184
+ #### Cross vs Isolated Margin
185
+
186
+ | Mode | Behavior | Use When |
187
+ |------|----------|----------|
188
+ | **Cross** | All positions share the same margin pool. One position's loss can liquidate everything. | Single position per exchange, or highly correlated positions |
189
+ | **Isolated** | Each position has its own margin. Liquidation of one doesn't affect others. | Multiple independent positions, recommended for arb |
190
+
191
+ **For funding rate arb, use ISOLATED margin.** Each leg should be independent — if one side gets liquidated, the other side survives.
192
+
193
+ ```bash
194
+ perp --json manage margin <SYM> isolated # set isolated margin
195
+ perp --json -e <EX> trade leverage <SYM> <LEV> --isolated # set leverage + isolated at once
196
+ ```
197
+
198
+ **Check current settings:**
199
+ ```bash
200
+ perp --json -e <EX> account settings # shows leverage and margin_mode per symbol
201
+ ```
202
+
203
+ ### Risk Limits — Configure Before Trading
204
+
205
+ Set your risk limits FIRST, before any trading activity:
206
+ ```bash
207
+ perp --json risk limits \
208
+ --max-leverage 5 \
209
+ --max-margin 60 \
210
+ --max-position 5000 \
211
+ --max-exposure 20000 \
212
+ --max-drawdown 500 \
213
+ --daily-loss 200 \
214
+ --min-liq-distance 30
215
+ ```
216
+
217
+ **IMPORTANT: Ask the user about their risk tolerance BEFORE setting limits.** Key questions:
218
+ - "How much leverage are you comfortable with?" (default: 5x for arb)
219
+ - "What's your maximum acceptable loss?" (default: $500)
220
+ - "How close to liquidation are you willing to get?" (default: 30%, minimum: 20%)
221
+
222
+ These limits are enforced by `perp risk check`. Always run it before trades:
223
+ ```bash
224
+ perp --json risk check --notional 1000 --leverage 3
225
+ # Returns: { "allowed": true/false, "reason": "...", "riskLevel": "low/medium/high/critical" }
226
+ ```
227
+
228
+ **If `allowed: false`, do NOT proceed.** Report to user why.
229
+
230
+ ### The Risk Monitoring Loop
231
+
232
+ **This is your primary responsibility.** While positions are open, run this loop continuously:
233
+
234
+ #### Every 15 minutes:
235
+ ```bash
236
+ perp --json risk status # overall risk level + violations
237
+ perp --json risk liquidation-distance # % distance from liq price for ALL positions
238
+ perp --json -e <EX> account positions # check each position's P&L
239
+ ```
240
+
241
+ Check the output:
242
+ - `risk status` returns `level` (low/medium/high/critical) and `canTrade` (boolean)
243
+ - If `level` is "high" or "critical" → take action immediately
244
+ - If `canTrade` is false → do NOT open new positions
245
+ - Check `violations[]` for specific issues
246
+
247
+ #### Every hour (at funding settlement):
248
+ ```bash
249
+ perp --json portfolio # total equity across exchanges
250
+ perp --json arb rates # are rates still favorable?
251
+ perp --json -e <EX_A> account positions # P&L on leg A
252
+ perp --json -e <EX_B> account positions # P&L on leg B
253
+ ```
254
+
255
+ Compare each position's unrealized P&L. In a perfect arb, they should roughly offset. If one side is losing significantly more than the other is gaining, investigate — the hedge may not be balanced.
256
+
257
+ #### Immediate action triggers:
258
+ | Condition | Action |
259
+ |-----------|--------|
260
+ | `risk status` level = "critical" | Reduce positions immediately |
261
+ | Liquidation price within 10% of current price | Reduce that position's size |
262
+ | `canTrade` = false | Stop all new trades, focus on reducing risk |
263
+ | One arb leg closed unexpectedly | Close the other leg IMMEDIATELY (naked exposure) |
264
+ | Unrealized loss > max-drawdown limit | Close losing positions |
265
+ | Margin utilization > 80% | Do not open new positions |
266
+
267
+ ### Position Sizing
268
+
269
+ Think in terms of total capital across all exchanges:
270
+ ```bash
271
+ perp --json portfolio # totalEquity, marginUtilization, concentration
272
+ ```
273
+
274
+ Rules of thumb:
275
+ - **Single position notional < 25% of total equity** across all exchanges
276
+ - **Total margin used < 60% of total equity** — leave buffer for adverse moves
277
+ - **Capital in transit (bridging) counts as "at risk"** — it's not available for margin
278
+
279
+ ### Stop Loss for Arb Positions
280
+
281
+ Even "market-neutral" arb can lose money if:
282
+ - One exchange goes down and you can't manage that leg
283
+ - Extreme funding spike in the wrong direction
284
+ - Slippage on entry/exit far exceeds estimates
285
+
286
+ **Always set stop losses on both legs:**
287
+ ```bash
288
+ perp --json -e <EX_A> trade sl <SYM> <PRICE> # stop loss on leg A
289
+ perp --json -e <EX_B> trade sl <SYM> <PRICE> # stop loss on leg B
290
+ ```
291
+
292
+ Or use TP/SL together:
293
+ ```bash
294
+ perp --json -e <EX> trade tp-sl <SYM> --tp <PRICE> --sl <PRICE>
295
+ ```
296
+
297
+ ### Monitoring Alerts
298
+
299
+ Set up alerts so you get notified of dangerous conditions without polling:
300
+ ```bash
301
+ perp --json alert add -t margin --margin-pct 70 # alert when margin usage > 70%
302
+ perp --json alert add -t price -s BTC --below 50000 --above 80000 # price boundaries
303
+ perp --json alert add -t funding -s ETH --spread 50 # funding spread alert
304
+ ```
305
+
306
+ ### What to Track Over Time
307
+ As you operate, build awareness of:
308
+ - Which symbols consistently have the best funding spreads
309
+ - Which exchange pairs have the lowest switching cost
310
+ - Typical bridge times for each route
311
+ - How quickly funding rate spikes mean-revert
312
+ - Your own execution quality (slippage vs estimates)
313
+ - **How close your positions have come to liquidation** — learn from near-misses
314
+
315
+ This is YOUR operational knowledge. Use it to make better decisions over time.
316
+
317
+ ## Capital Efficiency
318
+
319
+ ### Cross-Exchange Capital Allocation
320
+ Your capital is split across exchanges. Rebalancing has real costs:
321
+ ```bash
322
+ perp --json bridge quote --from solana --to arbitrum --amount 1000
323
+ ```
324
+
325
+ Before rebalancing, ask:
326
+ - Is the capital earning anything where it currently sits?
327
+ - Is the destination opportunity worth the bridge fee + downtime?
328
+ - Can I use the capital more efficiently without moving it?
329
+
330
+ ### Idle Capital
331
+ Capital sitting in an exchange wallet but not in a position is earning 0%.
332
+ Options:
333
+ - Open a low-risk funding collection position
334
+ - Bridge to where it's more useful
335
+ - Sometimes idle cash IS the right position (dry powder for opportunities)
336
+
337
+ ## Summary
338
+
339
+ Your job is not to blindly follow rules — it's to develop judgment:
340
+ - Every switch has a cost. Calculate it before acting.
341
+ - Rates change hourly. What's profitable now may not be in 2 hours.
342
+ - Build pattern recognition over time.
343
+ - The best arb is one you're already in, not one you're chasing.
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # Scan funding rate arbitrage opportunities
3
+ # Usage: ./arb-scan.sh [min-spread-bps]
4
+ # Example: ./arb-scan.sh 20
5
+
6
+ set -e
7
+
8
+ MIN_SPREAD="${1:-10}"
9
+
10
+ echo "=== Funding Rate Arbitrage Scanner ==="
11
+ echo "Minimum spread: ${MIN_SPREAD} bps"
12
+ echo ""
13
+
14
+ echo "1. Cross-exchange rates:"
15
+ perp --json arb rates
16
+
17
+ echo ""
18
+ echo "2. Opportunities (>= ${MIN_SPREAD} bps):"
19
+ perp --json arb scan --min "$MIN_SPREAD"
@@ -0,0 +1,38 @@
1
+ #!/bin/bash
2
+ # Open a perp position with pre-flight checks
3
+ # Usage: ./open-position.sh <exchange> <symbol> <side> <size>
4
+ # Example: ./open-position.sh hl BTC buy 0.01
5
+
6
+ set -e
7
+
8
+ EX="${1:?Usage: open-position.sh <exchange> <symbol> <side> <size>}"
9
+ SYM="${2:?Usage: open-position.sh <exchange> <symbol> <side> <size>}"
10
+ SIDE="${3:?Usage: open-position.sh <exchange> <symbol> <side> <size>}"
11
+ SIZE="${4:?Usage: open-position.sh <exchange> <symbol> <side> <size>}"
12
+
13
+ echo "=== Pre-flight checks ==="
14
+ echo "1. Account info:"
15
+ perp --json -e "$EX" account info
16
+
17
+ echo "2. Current price:"
18
+ perp --json -e "$EX" market mid "$SYM"
19
+
20
+ echo "3. Trade validation:"
21
+ perp --json -e "$EX" trade check "$SYM" "$SIDE" "$SIZE"
22
+
23
+ echo ""
24
+ echo "=== Ready to execute ==="
25
+ echo " Exchange: $EX"
26
+ echo " Symbol: $SYM"
27
+ echo " Side: $SIDE"
28
+ echo " Size: $SIZE"
29
+ echo ""
30
+ read -p "Confirm? (y/N): " CONFIRM
31
+
32
+ if [ "$CONFIRM" = "y" ] || [ "$CONFIRM" = "Y" ]; then
33
+ perp --json -e "$EX" trade market "$SYM" "$SIDE" "$SIZE"
34
+ echo "=== Position verification ==="
35
+ perp --json -e "$EX" account positions
36
+ else
37
+ echo "Cancelled."
38
+ fi
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ # Wallet setup for perp-cli
3
+ # Usage: ./wallet-setup.sh <exchange> <private-key>
4
+ # Exchanges: hl (Hyperliquid), pac (Pacifica), lt (Lighter)
5
+
6
+ set -e
7
+
8
+ EXCHANGE="${1:?Usage: wallet-setup.sh <exchange> <private-key>}"
9
+ KEY="${2:?Usage: wallet-setup.sh <exchange> <private-key>}"
10
+
11
+ echo "Setting up wallet for $EXCHANGE..."
12
+ perp --json wallet set "$EXCHANGE" "$KEY"
13
+
14
+ echo "Verifying..."
15
+ perp --json wallet show
16
+
17
+ echo "Done. Wallet configured for $EXCHANGE."