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 +12 -8
- package/dist/__tests__/risk-assessment.test.js +124 -3
- package/dist/commands/risk.js +106 -3
- package/dist/index.js +3 -3
- package/dist/mcp-server.js +1 -1
- package/dist/risk.d.ts +20 -0
- package/dist/risk.js +65 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +19 -11
- package/skills/perp-cli/references/agent-operations.md +243 -0
- package/skills/perp-cli/references/commands.md +5 -2
- package/skills/perp-cli/references/strategies.md +343 -0
- package/skills/perp-cli/templates/arb-scan.sh +19 -0
- package/skills/perp-cli/templates/open-position.sh +38 -0
- package/skills/perp-cli/templates/wallet-setup.sh +17 -0
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
|
|
181
|
+
Install as a Claude Code plugin or via the universal skills CLI:
|
|
182
182
|
|
|
183
183
|
```bash
|
|
184
|
-
#
|
|
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-
|
|
189
|
+
/plugin install perp-cli
|
|
187
190
|
```
|
|
188
191
|
|
|
189
|
-
Once installed,
|
|
192
|
+
Once installed, Claude can trade for you via natural language:
|
|
190
193
|
|
|
191
194
|
```
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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
|
+
});
|
package/dist/commands/risk.js
CHANGED
|
@@ -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 ||
|
|
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}
|
|
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.
|
|
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.
|
|
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.
|
|
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:"));
|
package/dist/mcp-server.js
CHANGED
|
@@ -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.
|
|
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
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -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.
|
|
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. **
|
|
18
|
-
2. **
|
|
19
|
-
3. **
|
|
20
|
-
4. **
|
|
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
|
|
78
|
-
2. perp --json -e <EX>
|
|
79
|
-
3. perp --json -e <EX>
|
|
80
|
-
4.
|
|
81
|
-
5. perp --json -e <EX> trade
|
|
82
|
-
6.
|
|
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
|
|
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."
|