perp-cli 0.4.2 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/arb/sizing.d.ts +12 -1
- package/dist/arb/sizing.js +38 -5
- package/dist/arb/state.d.ts +6 -0
- package/dist/cli-spec.d.ts +0 -4
- package/dist/cli-spec.js +8 -1
- package/dist/commands/agent.js +7 -4
- package/dist/commands/arb-auto.js +643 -4
- package/dist/commands/dex.js +7 -1
- package/dist/commands/funding.d.ts +2 -1
- package/dist/commands/funding.js +211 -4
- package/dist/commands/manage.js +1 -1
- package/dist/commands/market.js +1 -1
- package/dist/commands/multileg.d.ts +16 -0
- package/dist/commands/multileg.js +174 -0
- package/dist/commands/pnl.d.ts +12 -0
- package/dist/commands/pnl.js +243 -0
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.js +29 -8
- package/dist/commands/wallet.js +1 -1
- package/dist/config.d.ts +5 -0
- package/dist/config.js +12 -0
- package/dist/dashboard/ws-feeds.js +1 -1
- package/dist/equity-tracker.d.ts +57 -0
- package/dist/equity-tracker.js +209 -0
- package/dist/exchanges/hyperliquid-spot.d.ts +68 -0
- package/dist/exchanges/hyperliquid-spot.js +317 -0
- package/dist/exchanges/hyperliquid.d.ts +26 -5
- package/dist/exchanges/hyperliquid.js +171 -73
- package/dist/exchanges/interface.d.ts +1 -1
- package/dist/exchanges/lighter-spot.d.ts +55 -0
- package/dist/exchanges/lighter-spot.js +417 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +128 -35
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +52 -37
- package/dist/exchanges/spot-interface.d.ts +45 -0
- package/dist/exchanges/spot-interface.js +22 -0
- package/dist/execution-log.d.ts +2 -2
- package/dist/funding/rates.d.ts +36 -0
- package/dist/funding/rates.js +146 -0
- package/dist/index.js +147 -98
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/signer/evm-local.d.ts +12 -0
- package/dist/signer/evm-local.js +19 -0
- package/dist/signer/index.d.ts +3 -0
- package/dist/signer/index.js +2 -0
- package/dist/signer/interface.d.ts +31 -0
- package/dist/signer/interface.js +7 -0
- package/dist/signer/solana-local.d.ts +13 -0
- package/dist/signer/solana-local.js +19 -0
- package/dist/smart-order.js +14 -3
- package/dist/utils.js +7 -1
- package/dist/ws/market-feed.d.ts +74 -0
- package/dist/ws/market-feed.js +227 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +93 -4
- package/skills/perp-cli/references/commands.md +16 -1
- package/skills/perp-cli/references/strategies.md +1 -1
- package/skills/perp-cli/scripts/arb-monitor.sh +74 -0
- package/skills/perp-cli/scripts/funding-analysis.sh +85 -0
- package/skills/perp-cli/scripts/preflight.sh +98 -0
- package/skills/perp-cli/scripts/spot-perp-scan.sh +83 -0
- package/skills/perp-cli/scripts/validate-arb.sh +112 -0
- package/skills/perp-cli/templates/arb-scan.sh +0 -15
- package/skills/perp-cli/templates/open-position.sh +0 -38
- package/skills/perp-cli/templates/wallet-setup.sh +0 -17
package/dist/commands/dex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
3
|
-
import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
|
|
3
|
+
import { printJson, jsonOk, jsonError, makeTable, formatUsd, withJsonErrors } from "../utils.js";
|
|
4
4
|
export function registerDexCommands(program, getAdapter, isJson) {
|
|
5
5
|
const dex = program.command("dex").description("HIP-3 deployed perp dex commands (Hyperliquid)");
|
|
6
6
|
// ── dex list ──
|
|
@@ -11,6 +11,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
|
|
|
11
11
|
await withJsonErrors(isJson(), async () => {
|
|
12
12
|
const adapter = await getAdapter();
|
|
13
13
|
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
14
|
+
if (isJson())
|
|
15
|
+
return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
|
|
14
16
|
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
@@ -41,6 +43,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
|
|
|
41
43
|
await withJsonErrors(isJson(), async () => {
|
|
42
44
|
const adapter = await getAdapter();
|
|
43
45
|
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
46
|
+
if (isJson())
|
|
47
|
+
return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
|
|
44
48
|
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
45
49
|
return;
|
|
46
50
|
}
|
|
@@ -80,6 +84,8 @@ export function registerDexCommands(program, getAdapter, isJson) {
|
|
|
80
84
|
await withJsonErrors(isJson(), async () => {
|
|
81
85
|
const adapter = await getAdapter();
|
|
82
86
|
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
87
|
+
if (isJson())
|
|
88
|
+
return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
|
|
83
89
|
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
84
90
|
return;
|
|
85
91
|
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
|
|
2
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
3
|
+
export declare function registerFundingCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
|
package/dist/commands/funding.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { makeTable, formatPercent, formatUsd, printJson, jsonOk } from "../utils.js";
|
|
2
|
+
import { makeTable, formatPercent, formatUsd, formatPnl, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
|
|
3
3
|
import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
4
|
-
import { annualizeHourlyRate } from "../funding.js";
|
|
4
|
+
import { estimateHourlyFunding, annualizeHourlyRate, annualizeRate, toHourlyRate } from "../funding.js";
|
|
5
5
|
import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
|
|
6
|
-
|
|
6
|
+
const ALL_EXCHANGES = ["hyperliquid", "pacifica", "lighter"];
|
|
7
|
+
export function registerFundingCommands(program, isJson, getAdapterForExchange) {
|
|
7
8
|
const funding = program.command("funding").description("Funding rate comparison across exchanges");
|
|
8
9
|
// ── perp funding rates ── (default: show top symbols)
|
|
9
10
|
funding
|
|
@@ -38,8 +39,15 @@ export function registerFundingCommands(program, isJson) {
|
|
|
38
39
|
saveFundingSnapshot(allRates);
|
|
39
40
|
}
|
|
40
41
|
catch { /* non-critical */ }
|
|
41
|
-
if (isJson())
|
|
42
|
+
if (isJson()) {
|
|
43
|
+
// When --ndjson or --fields is used, return the symbols array directly
|
|
44
|
+
// so each symbol gets its own line / fields can be picked per element.
|
|
45
|
+
// Otherwise return the full snapshot (includes exchangeStatus metadata).
|
|
46
|
+
if (process.argv.includes("--ndjson") || process.argv.includes("--fields")) {
|
|
47
|
+
return printJson(jsonOk(snapshot.symbols));
|
|
48
|
+
}
|
|
42
49
|
return printJson(jsonOk(snapshot));
|
|
50
|
+
}
|
|
43
51
|
printSnapshotTable(snapshot);
|
|
44
52
|
printExchangeStatus(snapshot);
|
|
45
53
|
console.log(chalk.gray(" * Rates shown are current predictions. Actual settled rates may differ.\n"));
|
|
@@ -234,6 +242,205 @@ export function registerFundingCommands(program, isJson) {
|
|
|
234
242
|
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
235
243
|
}
|
|
236
244
|
});
|
|
245
|
+
// ── perp funding positions ── (funding impact on open positions)
|
|
246
|
+
funding
|
|
247
|
+
.command("positions")
|
|
248
|
+
.description("Show funding rate impact on your current positions")
|
|
249
|
+
.option("--exchanges <list>", "Comma-separated exchanges to check (default: all with keys)")
|
|
250
|
+
.action(async (opts) => {
|
|
251
|
+
if (!getAdapterForExchange) {
|
|
252
|
+
const msg = "funding positions requires adapter access";
|
|
253
|
+
if (isJson())
|
|
254
|
+
return printJson(jsonError("INTERNAL", msg));
|
|
255
|
+
console.log(chalk.red(` ${msg}\n`));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
await withJsonErrors(isJson(), async () => {
|
|
259
|
+
const targetExchanges = opts.exchanges
|
|
260
|
+
? opts.exchanges.split(",").map(s => s.trim().toLowerCase())
|
|
261
|
+
: [...ALL_EXCHANGES];
|
|
262
|
+
if (!isJson())
|
|
263
|
+
console.log(chalk.cyan(" Fetching positions and funding rates...\n"));
|
|
264
|
+
const results = [];
|
|
265
|
+
const exchangeErrors = {};
|
|
266
|
+
await Promise.all(targetExchanges.map(async (exchange) => {
|
|
267
|
+
try {
|
|
268
|
+
const adapter = await getAdapterForExchange(exchange);
|
|
269
|
+
const [positions, markets, fundingPayments] = await Promise.all([
|
|
270
|
+
adapter.getPositions(),
|
|
271
|
+
adapter.getMarkets(),
|
|
272
|
+
adapter.getFundingPayments(100).catch(() => []),
|
|
273
|
+
]);
|
|
274
|
+
if (positions.length === 0)
|
|
275
|
+
return;
|
|
276
|
+
// Build symbol → funding rate map
|
|
277
|
+
const fundingMap = new Map();
|
|
278
|
+
for (const m of markets) {
|
|
279
|
+
const sym = m.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
280
|
+
fundingMap.set(sym, {
|
|
281
|
+
rate: Number(m.fundingRate) || 0,
|
|
282
|
+
markPrice: Number(m.markPrice) || 0,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
// Aggregate actual funding payments per symbol (last 24h)
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const dayAgo = now - 24 * 60 * 60 * 1000;
|
|
288
|
+
const actualFunding = new Map();
|
|
289
|
+
for (const fp of fundingPayments) {
|
|
290
|
+
if (fp.time < dayAgo)
|
|
291
|
+
continue;
|
|
292
|
+
const sym = fp.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
293
|
+
const amt = Number(fp.payment) || 0;
|
|
294
|
+
if (!actualFunding.has(sym))
|
|
295
|
+
actualFunding.set(sym, { received: 0, paid: 0 });
|
|
296
|
+
const entry = actualFunding.get(sym);
|
|
297
|
+
if (amt > 0)
|
|
298
|
+
entry.received += amt;
|
|
299
|
+
else
|
|
300
|
+
entry.paid += Math.abs(amt);
|
|
301
|
+
}
|
|
302
|
+
for (const pos of positions) {
|
|
303
|
+
const sym = pos.symbol.toUpperCase().replace(/-PERP$/, "");
|
|
304
|
+
const fdata = fundingMap.get(sym);
|
|
305
|
+
const fundingRate = fdata?.rate ?? 0;
|
|
306
|
+
const mark = Number(pos.markPrice) || fdata?.markPrice || 0;
|
|
307
|
+
const size = Math.abs(Number(pos.size));
|
|
308
|
+
const notionalUsd = size * mark;
|
|
309
|
+
const hourlyRate = toHourlyRate(fundingRate, exchange);
|
|
310
|
+
const annualPct = annualizeRate(fundingRate, exchange);
|
|
311
|
+
const hourlyPayment = estimateHourlyFunding(fundingRate, exchange, notionalUsd, pos.side);
|
|
312
|
+
const dailyPayment = hourlyPayment * 24;
|
|
313
|
+
const actual = actualFunding.get(sym);
|
|
314
|
+
const actualReceived24h = actual?.received ?? 0;
|
|
315
|
+
const actualPaid24h = actual?.paid ?? 0;
|
|
316
|
+
const actualNet24h = actualReceived24h - actualPaid24h;
|
|
317
|
+
results.push({
|
|
318
|
+
exchange,
|
|
319
|
+
symbol: sym,
|
|
320
|
+
side: pos.side,
|
|
321
|
+
size: pos.size,
|
|
322
|
+
entryPrice: pos.entryPrice,
|
|
323
|
+
markPrice: pos.markPrice,
|
|
324
|
+
unrealizedPnl: pos.unrealizedPnl,
|
|
325
|
+
leverage: pos.leverage,
|
|
326
|
+
notionalUsd,
|
|
327
|
+
fundingRate,
|
|
328
|
+
hourlyRate,
|
|
329
|
+
annualPct,
|
|
330
|
+
hourlyPayment,
|
|
331
|
+
dailyPayment,
|
|
332
|
+
actualReceived24h,
|
|
333
|
+
actualPaid24h,
|
|
334
|
+
actualNet24h,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (err) {
|
|
339
|
+
exchangeErrors[exchange] = err instanceof Error ? err.message : String(err);
|
|
340
|
+
}
|
|
341
|
+
}));
|
|
342
|
+
if (results.length === 0) {
|
|
343
|
+
if (isJson()) {
|
|
344
|
+
return printJson(jsonOk({
|
|
345
|
+
positions: [],
|
|
346
|
+
totals: {
|
|
347
|
+
predicted: { hourly: 0, daily: 0 },
|
|
348
|
+
actual24h: { net: 0 },
|
|
349
|
+
notionalUsd: 0,
|
|
350
|
+
},
|
|
351
|
+
errors: exchangeErrors,
|
|
352
|
+
}));
|
|
353
|
+
}
|
|
354
|
+
console.log(chalk.gray(" No open positions found on any exchange.\n"));
|
|
355
|
+
if (Object.keys(exchangeErrors).length > 0) {
|
|
356
|
+
for (const [ex, err] of Object.entries(exchangeErrors)) {
|
|
357
|
+
console.log(chalk.yellow(` ${ex}: ${err}`));
|
|
358
|
+
}
|
|
359
|
+
console.log();
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
// Sort: biggest daily payment impact first
|
|
364
|
+
results.sort((a, b) => Math.abs(b.dailyPayment) - Math.abs(a.dailyPayment));
|
|
365
|
+
const totalHourly = results.reduce((s, r) => s + r.hourlyPayment, 0);
|
|
366
|
+
const totalDaily = results.reduce((s, r) => s + r.dailyPayment, 0);
|
|
367
|
+
const totalNotional = results.reduce((s, r) => s + r.notionalUsd, 0);
|
|
368
|
+
const totalActualNet = results.reduce((s, r) => s + r.actualNet24h, 0);
|
|
369
|
+
if (isJson()) {
|
|
370
|
+
return printJson(jsonOk({
|
|
371
|
+
positions: results.map(r => ({
|
|
372
|
+
exchange: r.exchange,
|
|
373
|
+
symbol: r.symbol,
|
|
374
|
+
side: r.side,
|
|
375
|
+
size: r.size,
|
|
376
|
+
notionalUsd: Number(r.notionalUsd.toFixed(2)),
|
|
377
|
+
fundingRate: r.fundingRate,
|
|
378
|
+
annualPct: Number(r.annualPct.toFixed(2)),
|
|
379
|
+
predicted: {
|
|
380
|
+
hourly: Number(r.hourlyPayment.toFixed(6)),
|
|
381
|
+
daily: Number(r.dailyPayment.toFixed(4)),
|
|
382
|
+
},
|
|
383
|
+
actual24h: {
|
|
384
|
+
received: Number(r.actualReceived24h.toFixed(6)),
|
|
385
|
+
paid: Number(r.actualPaid24h.toFixed(6)),
|
|
386
|
+
net: Number(r.actualNet24h.toFixed(6)),
|
|
387
|
+
},
|
|
388
|
+
unrealizedPnl: r.unrealizedPnl,
|
|
389
|
+
})),
|
|
390
|
+
totals: {
|
|
391
|
+
predicted: {
|
|
392
|
+
hourly: Number(totalHourly.toFixed(6)),
|
|
393
|
+
daily: Number(totalDaily.toFixed(4)),
|
|
394
|
+
},
|
|
395
|
+
actual24h: {
|
|
396
|
+
net: Number(totalActualNet.toFixed(6)),
|
|
397
|
+
},
|
|
398
|
+
notionalUsd: Number(totalNotional.toFixed(2)),
|
|
399
|
+
},
|
|
400
|
+
errors: Object.keys(exchangeErrors).length > 0 ? exchangeErrors : undefined,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
|
|
404
|
+
console.log(chalk.cyan.bold(" Position Funding Impact\n"));
|
|
405
|
+
const rows = results.map(r => {
|
|
406
|
+
const sideColor = r.side === "long" ? chalk.green : chalk.red;
|
|
407
|
+
const rateColor = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white;
|
|
408
|
+
// actual net: positive = received, negative = paid
|
|
409
|
+
const actualColor = r.actualNet24h > 0 ? chalk.green : r.actualNet24h < 0 ? chalk.red : chalk.gray;
|
|
410
|
+
const actualStr = r.actualNet24h !== 0
|
|
411
|
+
? actualColor(`${r.actualNet24h > 0 ? "+" : ""}$${r.actualNet24h.toFixed(4)}`)
|
|
412
|
+
: chalk.gray("-");
|
|
413
|
+
return [
|
|
414
|
+
chalk.white.bold(exAbbr(r.exchange)),
|
|
415
|
+
chalk.white.bold(r.symbol),
|
|
416
|
+
sideColor(r.side.toUpperCase()),
|
|
417
|
+
`$${formatUsd(r.notionalUsd)}`,
|
|
418
|
+
rateColor(formatPercent(r.fundingRate)),
|
|
419
|
+
rateColor(`${r.annualPct.toFixed(1)}%`),
|
|
420
|
+
actualStr,
|
|
421
|
+
formatPnl(r.unrealizedPnl),
|
|
422
|
+
];
|
|
423
|
+
});
|
|
424
|
+
console.log(makeTable(["Ex", "Symbol", "Side", "Notional", "Rate(now)", "Annual(now)", "Actual 24h", "uPnL"], rows));
|
|
425
|
+
// Summary
|
|
426
|
+
const actualNetColor = totalActualNet >= 0 ? chalk.green : chalk.red;
|
|
427
|
+
const predictColor = totalDaily >= 0 ? chalk.red : chalk.green;
|
|
428
|
+
const predictSign = totalDaily >= 0 ? "-" : "+";
|
|
429
|
+
console.log();
|
|
430
|
+
console.log(` Total Notional: $${formatUsd(totalNotional)}`);
|
|
431
|
+
console.log(` Actual Net (24h): ${actualNetColor(`${totalActualNet >= 0 ? "+" : ""}$${totalActualNet.toFixed(4)}`)}`);
|
|
432
|
+
console.log(` Predicted (now rate): ${predictColor(`${predictSign}$${Math.abs(totalDaily).toFixed(4)}/d`)}`);
|
|
433
|
+
if (Object.keys(exchangeErrors).length > 0) {
|
|
434
|
+
console.log();
|
|
435
|
+
for (const [ex, err] of Object.entries(exchangeErrors)) {
|
|
436
|
+
console.log(chalk.yellow(` ${exAbbr(ex)}: ${err}`));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
console.log(chalk.gray("\n Actual 24h: real funding received/paid in the last 24 hours."));
|
|
440
|
+
console.log(chalk.gray(" Predicted: based on current rate only (changes every hour)."));
|
|
441
|
+
console.log(chalk.gray(" + = received, - = paid\n"));
|
|
442
|
+
});
|
|
443
|
+
});
|
|
237
444
|
}
|
|
238
445
|
// ── Display helpers ──
|
|
239
446
|
function formatAvgRate(rate) {
|
package/dist/commands/manage.js
CHANGED
|
@@ -274,7 +274,7 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
|
|
|
274
274
|
manage
|
|
275
275
|
.command("setup-api-key")
|
|
276
276
|
.description("Generate & register a Lighter API key (required for trading)")
|
|
277
|
-
.option("--key-index <n>", "API key index (
|
|
277
|
+
.option("--key-index <n>", "API key index (4-254, default: 4)", "4")
|
|
278
278
|
.action(async (opts) => {
|
|
279
279
|
const adapter = await getAdapter();
|
|
280
280
|
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
package/dist/commands/market.js
CHANGED
|
@@ -139,7 +139,7 @@ export function registerMarketCommands(program, getAdapter, isJson) {
|
|
|
139
139
|
const rows = history.map((h) => [
|
|
140
140
|
new Date(h.time).toLocaleString(),
|
|
141
141
|
formatPercent(h.rate),
|
|
142
|
-
`$${formatUsd(h.price)}
|
|
142
|
+
h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"),
|
|
143
143
|
]);
|
|
144
144
|
console.log(makeTable(["Time", "Funding Rate", "Oracle"], rows));
|
|
145
145
|
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-leg order command.
|
|
3
|
+
*
|
|
4
|
+
* Executes multiple orders across exchanges simultaneously using Promise.allSettled.
|
|
5
|
+
* Supports automatic rollback if one leg fails.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* perp multi "hl:ETH:buy:0.01" "pac:ETH:sell:0.01"
|
|
9
|
+
* perp multi "hl:BTC:buy:0.001" "lighter:BTC:sell:0.001" --smart
|
|
10
|
+
* perp multi "hl:ETH:buy:0.01" "pac:SOL:buy:1" "lighter:BTC:sell:0.0001"
|
|
11
|
+
*
|
|
12
|
+
* Leg format: exchange:symbol:side:size
|
|
13
|
+
*/
|
|
14
|
+
import { Command } from "commander";
|
|
15
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
16
|
+
export declare function registerMultilegCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-leg order command.
|
|
3
|
+
*
|
|
4
|
+
* Executes multiple orders across exchanges simultaneously using Promise.allSettled.
|
|
5
|
+
* Supports automatic rollback if one leg fails.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* perp multi "hl:ETH:buy:0.01" "pac:ETH:sell:0.01"
|
|
9
|
+
* perp multi "hl:BTC:buy:0.001" "lighter:BTC:sell:0.001" --smart
|
|
10
|
+
* perp multi "hl:ETH:buy:0.01" "pac:SOL:buy:1" "lighter:BTC:sell:0.0001"
|
|
11
|
+
*
|
|
12
|
+
* Leg format: exchange:symbol:side:size
|
|
13
|
+
*/
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import { printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
|
|
16
|
+
import { logExecution } from "../execution-log.js";
|
|
17
|
+
import { smartOrder } from "../smart-order.js";
|
|
18
|
+
function parseLeg(spec) {
|
|
19
|
+
const parts = spec.split(":");
|
|
20
|
+
if (parts.length !== 4) {
|
|
21
|
+
throw new Error(`Invalid leg format "${spec}" — expected exchange:symbol:side:size`);
|
|
22
|
+
}
|
|
23
|
+
const [exchange, symbol, side, size] = parts;
|
|
24
|
+
// Normalize exchange aliases
|
|
25
|
+
const exMap = {
|
|
26
|
+
hl: "hyperliquid", pac: "pacifica", lit: "lighter",
|
|
27
|
+
hyperliquid: "hyperliquid", pacifica: "pacifica", lighter: "lighter",
|
|
28
|
+
};
|
|
29
|
+
const normalizedExchange = exMap[exchange.toLowerCase()];
|
|
30
|
+
if (!normalizedExchange) {
|
|
31
|
+
throw new Error(`Unknown exchange "${exchange}" in leg spec`);
|
|
32
|
+
}
|
|
33
|
+
if (side !== "buy" && side !== "sell") {
|
|
34
|
+
throw new Error(`Invalid side "${side}" — must be "buy" or "sell"`);
|
|
35
|
+
}
|
|
36
|
+
if (isNaN(Number(size)) || Number(size) <= 0) {
|
|
37
|
+
throw new Error(`Invalid size "${size}" — must be a positive number`);
|
|
38
|
+
}
|
|
39
|
+
return { exchange: normalizedExchange, symbol: symbol.toUpperCase(), side, size };
|
|
40
|
+
}
|
|
41
|
+
export function registerMultilegCommands(program, getAdapterForExchange, isJson) {
|
|
42
|
+
program
|
|
43
|
+
.command("multi <legs...>")
|
|
44
|
+
.description("Execute multi-leg orders simultaneously (exchange:symbol:side:size)")
|
|
45
|
+
.option("--smart", "Use smart order (IOC limit + fallback) for each leg")
|
|
46
|
+
.option("--rollback", "Auto-rollback filled legs if any leg fails", true)
|
|
47
|
+
.option("--no-rollback", "Disable auto-rollback")
|
|
48
|
+
.option("--timeout <ms>", "Per-leg timeout in milliseconds", "30000")
|
|
49
|
+
.action(async (legSpecs, opts) => {
|
|
50
|
+
await withJsonErrors(isJson(), async () => {
|
|
51
|
+
// Parse legs
|
|
52
|
+
const legs = legSpecs.map(parseLeg);
|
|
53
|
+
if (legs.length < 2) {
|
|
54
|
+
const err = "Multi-leg requires at least 2 legs";
|
|
55
|
+
if (isJson()) {
|
|
56
|
+
printJson(jsonError("INVALID_ARGS", err));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
throw new Error(err);
|
|
60
|
+
}
|
|
61
|
+
if (!isJson()) {
|
|
62
|
+
console.log(chalk.cyan.bold("Multi-Leg Order\n"));
|
|
63
|
+
for (const l of legs) {
|
|
64
|
+
const color = l.side === "buy" ? chalk.green : chalk.red;
|
|
65
|
+
console.log(` ${l.exchange.padEnd(14)} ${color(l.side.toUpperCase())} ${l.symbol} x ${l.size}`);
|
|
66
|
+
}
|
|
67
|
+
console.log();
|
|
68
|
+
}
|
|
69
|
+
// Get adapters (deduplicated)
|
|
70
|
+
const adapters = new Map();
|
|
71
|
+
for (const l of legs) {
|
|
72
|
+
if (!adapters.has(l.exchange)) {
|
|
73
|
+
adapters.set(l.exchange, await getAdapterForExchange(l.exchange));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// Execute all legs simultaneously
|
|
77
|
+
const timeoutMs = parseInt(opts.timeout);
|
|
78
|
+
const results = await Promise.allSettled(legs.map(async (leg) => {
|
|
79
|
+
const adapter = adapters.get(leg.exchange);
|
|
80
|
+
const orderPromise = opts.smart
|
|
81
|
+
? smartOrder(adapter, leg.symbol, leg.side, leg.size).then(r => r.result)
|
|
82
|
+
: adapter.marketOrder(leg.symbol, leg.side, leg.size);
|
|
83
|
+
// Apply timeout
|
|
84
|
+
const result = await Promise.race([
|
|
85
|
+
orderPromise,
|
|
86
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Leg timeout")), timeoutMs)),
|
|
87
|
+
]);
|
|
88
|
+
// Log execution
|
|
89
|
+
logExecution({
|
|
90
|
+
type: "multi_leg",
|
|
91
|
+
exchange: leg.exchange,
|
|
92
|
+
symbol: leg.symbol,
|
|
93
|
+
side: leg.side,
|
|
94
|
+
size: leg.size,
|
|
95
|
+
status: "success",
|
|
96
|
+
dryRun: false,
|
|
97
|
+
meta: { result, smart: opts.smart },
|
|
98
|
+
});
|
|
99
|
+
return { leg, status: "filled", result };
|
|
100
|
+
}));
|
|
101
|
+
// Process results
|
|
102
|
+
const legResults = results.map((r, i) => {
|
|
103
|
+
if (r.status === "fulfilled")
|
|
104
|
+
return r.value;
|
|
105
|
+
return {
|
|
106
|
+
leg: legs[i],
|
|
107
|
+
status: "failed",
|
|
108
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
const filled = legResults.filter(r => r.status === "filled");
|
|
112
|
+
const failed = legResults.filter(r => r.status === "failed");
|
|
113
|
+
// Rollback if partial fill and rollback enabled
|
|
114
|
+
if (failed.length > 0 && filled.length > 0 && opts.rollback) {
|
|
115
|
+
if (!isJson()) {
|
|
116
|
+
console.log(chalk.yellow(`\n ${failed.length} leg(s) failed — rolling back ${filled.length} filled leg(s)...\n`));
|
|
117
|
+
}
|
|
118
|
+
for (const fr of filled) {
|
|
119
|
+
const adapter = adapters.get(fr.leg.exchange);
|
|
120
|
+
const reverseSide = fr.leg.side === "buy" ? "sell" : "buy";
|
|
121
|
+
try {
|
|
122
|
+
await adapter.marketOrder(fr.leg.symbol, reverseSide, fr.leg.size);
|
|
123
|
+
fr.status = "rolled_back";
|
|
124
|
+
logExecution({
|
|
125
|
+
type: "multi_leg_rollback",
|
|
126
|
+
exchange: fr.leg.exchange,
|
|
127
|
+
symbol: fr.leg.symbol,
|
|
128
|
+
side: reverseSide,
|
|
129
|
+
size: fr.leg.size,
|
|
130
|
+
status: "success",
|
|
131
|
+
dryRun: false,
|
|
132
|
+
});
|
|
133
|
+
if (!isJson()) {
|
|
134
|
+
console.log(chalk.gray(` Rolled back: ${fr.leg.exchange} ${reverseSide} ${fr.leg.symbol} x ${fr.leg.size}`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (!isJson()) {
|
|
139
|
+
console.log(chalk.red(` Rollback failed: ${fr.leg.exchange} ${fr.leg.symbol} — ${err instanceof Error ? err.message : err}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (isJson()) {
|
|
145
|
+
printJson(jsonOk({
|
|
146
|
+
legs: legResults.map(r => ({
|
|
147
|
+
exchange: r.leg.exchange,
|
|
148
|
+
symbol: r.leg.symbol,
|
|
149
|
+
side: r.leg.side,
|
|
150
|
+
size: r.leg.size,
|
|
151
|
+
status: r.status,
|
|
152
|
+
error: r.error,
|
|
153
|
+
})),
|
|
154
|
+
summary: {
|
|
155
|
+
total: legs.length,
|
|
156
|
+
filled: legResults.filter(r => r.status === "filled").length,
|
|
157
|
+
failed: failed.length,
|
|
158
|
+
rolledBack: legResults.filter(r => r.status === "rolled_back").length,
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.log(chalk.cyan.bold("\nResults:\n"));
|
|
164
|
+
for (const r of legResults) {
|
|
165
|
+
const icon = r.status === "filled" ? chalk.green("OK") :
|
|
166
|
+
r.status === "rolled_back" ? chalk.yellow("ROLLBACK") :
|
|
167
|
+
chalk.red("FAIL");
|
|
168
|
+
console.log(` ${icon} ${r.leg.exchange.padEnd(14)} ${r.leg.side} ${r.leg.symbol} x ${r.leg.size}${r.error ? ` — ${r.error}` : ""}`);
|
|
169
|
+
}
|
|
170
|
+
console.log(`\n Total: ${legs.length} Filled: ${filled.length} Failed: ${failed.length} Rolled back: ${legResults.filter(r => r.status === "rolled_back").length}`);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PnL Tracker CLI commands.
|
|
3
|
+
*
|
|
4
|
+
* perp pnl snapshot — take one-time equity snapshots across exchanges
|
|
5
|
+
* perp pnl track — continuous equity tracking (foreground)
|
|
6
|
+
* perp pnl daily — daily PnL breakdown table
|
|
7
|
+
* perp pnl weekly — weekly PnL breakdown table
|
|
8
|
+
* perp pnl summary — performance summary (Sharpe, drawdown, win rate)
|
|
9
|
+
*/
|
|
10
|
+
import { Command } from "commander";
|
|
11
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
12
|
+
export declare function registerPnlCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;
|