perp-cli 0.4.2 → 0.4.9
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 +46 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -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/account.d.ts +1 -1
- package/dist/commands/account.js +300 -63
- package/dist/commands/agent.js +16 -9
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +696 -9
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- 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/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +6 -4
- package/dist/commands/market.js +35 -2
- 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/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/stream.js +132 -55
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +270 -267
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +11 -1
- package/dist/config.d.ts +6 -1
- package/dist/config.js +21 -1
- 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 +69 -0
- package/dist/exchanges/hyperliquid-spot.js +309 -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 +415 -0
- package/dist/exchanges/lighter.d.ts +10 -2
- package/dist/exchanges/lighter.js +210 -56
- package/dist/exchanges/pacifica.d.ts +11 -4
- package/dist/exchanges/pacifica.js +64 -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 +126 -161
- package/dist/liquidity.d.ts +12 -0
- package/dist/liquidity.js +43 -0
- package/dist/mcp-server.js +43 -96
- 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/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- 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 +80 -4
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +54 -32
- package/skills/perp-cli/references/strategies.md +27 -17
- 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
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
|
|
2
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
3
|
+
export declare function registerHistoryCommands(program: Command, isJson: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;
|
package/dist/commands/history.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { printJson, jsonOk, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
|
|
2
|
+
import { printJson, jsonOk, jsonError, makeTable, formatUsd, formatPnl, withJsonErrors } from "../utils.js";
|
|
3
3
|
import { readExecutionLog, getExecutionStats, pruneExecutionLog } from "../execution-log.js";
|
|
4
4
|
import { readPositionHistory, getPositionStats } from "../position-history.js";
|
|
5
|
-
|
|
5
|
+
import { saveEquitySnapshot, readEquityHistory, computePnlMetrics, computeDailyPnl, aggregateWeekly, } from "../equity-tracker.js";
|
|
6
|
+
const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
|
|
7
|
+
function parseSince(since) {
|
|
8
|
+
if (!since)
|
|
9
|
+
return undefined;
|
|
10
|
+
const match = since.match(/^(\d+)(h|d|w)$/);
|
|
11
|
+
if (match) {
|
|
12
|
+
const [, num, unit] = match;
|
|
13
|
+
const ms = { h: 3600000, d: 86400000, w: 604800000 }[unit] ?? 86400000;
|
|
14
|
+
return new Date(Date.now() - parseInt(num) * ms).toISOString();
|
|
15
|
+
}
|
|
16
|
+
return since;
|
|
17
|
+
}
|
|
18
|
+
export function registerHistoryCommands(program, isJson, getAdapterForExchange) {
|
|
6
19
|
const history = program.command("history").description("Execution log & audit trail");
|
|
7
20
|
// ── history list ──
|
|
8
21
|
history
|
|
@@ -66,10 +79,10 @@ export function registerHistoryCommands(program, isJson) {
|
|
|
66
79
|
console.log(makeTable(["Time", "Exchange", "Type", "Symbol", "Side", "Size", "Notional", "Status", ""], rows));
|
|
67
80
|
});
|
|
68
81
|
});
|
|
69
|
-
// ── history stats ──
|
|
70
|
-
history
|
|
82
|
+
// ── history stats (hidden alias for 'history summary') ──
|
|
83
|
+
const statsCmd = history
|
|
71
84
|
.command("stats")
|
|
72
|
-
.description("
|
|
85
|
+
.description("Use 'history summary'")
|
|
73
86
|
.option("--since <date>", "Stats since date (ISO or relative: 24h, 7d, 30d)")
|
|
74
87
|
.action(async (opts) => {
|
|
75
88
|
await withJsonErrors(isJson(), async () => {
|
|
@@ -110,6 +123,7 @@ export function registerHistoryCommands(program, isJson) {
|
|
|
110
123
|
console.log();
|
|
111
124
|
});
|
|
112
125
|
});
|
|
126
|
+
statsCmd._hidden = true;
|
|
113
127
|
// ── history positions ──
|
|
114
128
|
history
|
|
115
129
|
.command("positions")
|
|
@@ -232,4 +246,493 @@ export function registerHistoryCommands(program, isJson) {
|
|
|
232
246
|
}
|
|
233
247
|
console.log(chalk.green(`\n Pruned ${pruned} old records (keeping last ${opts.keepDays} days).\n`));
|
|
234
248
|
});
|
|
249
|
+
// ── Analytics subcommands (merged from analytics.ts) ──
|
|
250
|
+
if (getAdapterForExchange) {
|
|
251
|
+
registerAnalyticsSubcommands(history, getAdapterForExchange, isJson);
|
|
252
|
+
// Keep deprecated top-level 'analytics' alias (hidden from help)
|
|
253
|
+
const analytics = program.command("analytics").description("Use 'perp history'");
|
|
254
|
+
analytics._hidden = true;
|
|
255
|
+
registerAnalyticsSubcommands(analytics, getAdapterForExchange, isJson);
|
|
256
|
+
}
|
|
257
|
+
// ── PnL subcommands (merged from pnl.ts) ──
|
|
258
|
+
if (getAdapterForExchange) {
|
|
259
|
+
registerPnlSubcommands(history, getAdapterForExchange, isJson);
|
|
260
|
+
// Deprecated top-level pnl alias
|
|
261
|
+
const pnlAlias = program.command("pnl").description("Use 'perp history perf --period daily|weekly|summary'");
|
|
262
|
+
pnlAlias._hidden = true;
|
|
263
|
+
if (getAdapterForExchange)
|
|
264
|
+
registerPnlSubcommands(pnlAlias, getAdapterForExchange, isJson);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function registerAnalyticsSubcommands(parent, getAdapterForExchange, isJson) {
|
|
268
|
+
// ── analytics summary ──
|
|
269
|
+
parent
|
|
270
|
+
.command("summary")
|
|
271
|
+
.description("Execution log summary statistics")
|
|
272
|
+
.option("--since <period>", "Period: 24h, 7d, 30d, or ISO date")
|
|
273
|
+
.action(async (opts) => {
|
|
274
|
+
await withJsonErrors(isJson(), async () => {
|
|
275
|
+
const since = parseSince(opts.since);
|
|
276
|
+
const stats = getExecutionStats(since);
|
|
277
|
+
const records = readExecutionLog({ since });
|
|
278
|
+
// Calculate volume
|
|
279
|
+
let totalVolume = 0;
|
|
280
|
+
for (const r of records) {
|
|
281
|
+
if (r.notional)
|
|
282
|
+
totalVolume += r.notional;
|
|
283
|
+
else if (r.price && r.size)
|
|
284
|
+
totalVolume += Number(r.price) * Number(r.size);
|
|
285
|
+
}
|
|
286
|
+
const result = { ...stats, totalVolume, period: opts.since ?? "all time" };
|
|
287
|
+
if (isJson())
|
|
288
|
+
return printJson(jsonOk(result));
|
|
289
|
+
console.log(chalk.cyan.bold(`\n Trading Summary ${opts.since ? `(${opts.since})` : "(all time)"}\n`));
|
|
290
|
+
console.log(` Total Trades: ${stats.totalTrades}`);
|
|
291
|
+
console.log(` Success Rate: ${stats.successRate.toFixed(1)}%`);
|
|
292
|
+
console.log(` Total Volume: $${formatUsd(totalVolume)}`);
|
|
293
|
+
if (Object.keys(stats.byExchange).length > 0) {
|
|
294
|
+
console.log(chalk.white.bold("\n By Exchange:"));
|
|
295
|
+
for (const [ex, count] of Object.entries(stats.byExchange)) {
|
|
296
|
+
console.log(` ${ex.padEnd(14)} ${count} trades`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (Object.keys(stats.byType).length > 0) {
|
|
300
|
+
console.log(chalk.white.bold("\n By Type:"));
|
|
301
|
+
for (const [type, count] of Object.entries(stats.byType)) {
|
|
302
|
+
console.log(` ${type.replace(/_/g, " ").padEnd(14)} ${count}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (stats.recentErrors.length > 0) {
|
|
306
|
+
console.log(chalk.red.bold("\n Recent Errors:"));
|
|
307
|
+
for (const err of stats.recentErrors) {
|
|
308
|
+
console.log(` ${chalk.red(err)}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
// ── analytics pnl ──
|
|
315
|
+
parent
|
|
316
|
+
.command("pnl")
|
|
317
|
+
.description("Realized P&L from exchange trade history (volume & fees by symbol/exchange)")
|
|
318
|
+
.option("-e, --exchange <exchanges>", "Comma-separated exchanges")
|
|
319
|
+
.option("--since <period>", "Period: 24h, 7d, 30d")
|
|
320
|
+
.option("-n, --limit <n>", "Trade history limit per exchange", "100")
|
|
321
|
+
.action(async (opts) => {
|
|
322
|
+
await withJsonErrors(isJson(), async () => {
|
|
323
|
+
const exchanges = opts.exchange
|
|
324
|
+
? opts.exchange.split(",").map(e => e.trim())
|
|
325
|
+
: [...EXCHANGES];
|
|
326
|
+
const limit = parseInt(opts.limit);
|
|
327
|
+
const sinceMs = opts.since ? new Date(parseSince(opts.since)).getTime() : 0;
|
|
328
|
+
const allTrades = [];
|
|
329
|
+
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
330
|
+
try {
|
|
331
|
+
const adapter = await getAdapterForExchange(ex);
|
|
332
|
+
const trades = await adapter.getTradeHistory(limit);
|
|
333
|
+
for (const t of trades) {
|
|
334
|
+
if (sinceMs && t.time < sinceMs)
|
|
335
|
+
continue;
|
|
336
|
+
allTrades.push({
|
|
337
|
+
exchange: ex,
|
|
338
|
+
symbol: t.symbol,
|
|
339
|
+
side: t.side,
|
|
340
|
+
price: Number(t.price),
|
|
341
|
+
size: Number(t.size),
|
|
342
|
+
fee: Number(t.fee),
|
|
343
|
+
time: t.time,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
catch { /* skip unavailable exchanges */ }
|
|
348
|
+
}));
|
|
349
|
+
// Aggregate by symbol
|
|
350
|
+
const bySymbol = new Map();
|
|
351
|
+
const byExchange = new Map();
|
|
352
|
+
let totalVolume = 0;
|
|
353
|
+
let totalFees = 0;
|
|
354
|
+
for (const t of allTrades) {
|
|
355
|
+
const notional = t.price * t.size;
|
|
356
|
+
totalVolume += notional;
|
|
357
|
+
totalFees += t.fee;
|
|
358
|
+
const sym = bySymbol.get(t.symbol) ?? { volume: 0, fees: 0, trades: 0 };
|
|
359
|
+
sym.volume += notional;
|
|
360
|
+
sym.fees += t.fee;
|
|
361
|
+
sym.trades++;
|
|
362
|
+
bySymbol.set(t.symbol, sym);
|
|
363
|
+
const ex = byExchange.get(t.exchange) ?? { volume: 0, fees: 0, trades: 0 };
|
|
364
|
+
ex.volume += notional;
|
|
365
|
+
ex.fees += t.fee;
|
|
366
|
+
ex.trades++;
|
|
367
|
+
byExchange.set(t.exchange, ex);
|
|
368
|
+
}
|
|
369
|
+
const result = {
|
|
370
|
+
totalTrades: allTrades.length,
|
|
371
|
+
totalVolume,
|
|
372
|
+
totalFees,
|
|
373
|
+
netAfterFees: -totalFees, // realized PnL would need entry/exit matching; fees are definite cost
|
|
374
|
+
bySymbol: Object.fromEntries(bySymbol),
|
|
375
|
+
byExchange: Object.fromEntries(byExchange),
|
|
376
|
+
};
|
|
377
|
+
if (isJson())
|
|
378
|
+
return printJson(jsonOk(result));
|
|
379
|
+
console.log(chalk.cyan.bold(`\n Realized Trading P&L ${opts.since ? `(${opts.since})` : ""}\n`));
|
|
380
|
+
console.log(` Total Trades: ${allTrades.length}`);
|
|
381
|
+
console.log(` Total Volume: $${formatUsd(totalVolume)}`);
|
|
382
|
+
console.log(` Total Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
|
|
383
|
+
if (bySymbol.size > 0) {
|
|
384
|
+
console.log(chalk.white.bold("\n By Symbol:"));
|
|
385
|
+
const symRows = [...bySymbol.entries()]
|
|
386
|
+
.sort((a, b) => b[1].volume - a[1].volume)
|
|
387
|
+
.map(([sym, d]) => [
|
|
388
|
+
chalk.white.bold(sym),
|
|
389
|
+
String(d.trades),
|
|
390
|
+
`$${formatUsd(d.volume)}`,
|
|
391
|
+
chalk.red(`-$${formatUsd(d.fees)}`),
|
|
392
|
+
]);
|
|
393
|
+
console.log(makeTable(["Symbol", "Trades", "Volume", "Fees"], symRows));
|
|
394
|
+
}
|
|
395
|
+
if (byExchange.size > 0) {
|
|
396
|
+
console.log(chalk.white.bold(" By Exchange:"));
|
|
397
|
+
const exRows = [...byExchange.entries()].map(([ex, d]) => [
|
|
398
|
+
chalk.white.bold(ex),
|
|
399
|
+
String(d.trades),
|
|
400
|
+
`$${formatUsd(d.volume)}`,
|
|
401
|
+
chalk.red(`-$${formatUsd(d.fees)}`),
|
|
402
|
+
]);
|
|
403
|
+
console.log(makeTable(["Exchange", "Trades", "Volume", "Fees"], exRows));
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
// ── analytics funding ──
|
|
408
|
+
parent
|
|
409
|
+
.command("funding")
|
|
410
|
+
.description("Funding payment aggregation across exchanges (net by symbol/exchange)")
|
|
411
|
+
.option("-e, --exchange <exchanges>", "Comma-separated exchanges")
|
|
412
|
+
.option("-s, --symbol <symbol>", "Filter by symbol")
|
|
413
|
+
.option("-n, --limit <n>", "Funding history limit per exchange", "50")
|
|
414
|
+
.action(async (opts) => {
|
|
415
|
+
await withJsonErrors(isJson(), async () => {
|
|
416
|
+
const exchanges = opts.exchange
|
|
417
|
+
? opts.exchange.split(",").map(e => e.trim())
|
|
418
|
+
: [...EXCHANGES];
|
|
419
|
+
const allFunding = [];
|
|
420
|
+
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
421
|
+
try {
|
|
422
|
+
const adapter = await getAdapterForExchange(ex);
|
|
423
|
+
const payments = await adapter.getFundingPayments(parseInt(opts.limit));
|
|
424
|
+
for (const p of payments) {
|
|
425
|
+
if (opts.symbol && !p.symbol.toUpperCase().includes(opts.symbol.toUpperCase()))
|
|
426
|
+
continue;
|
|
427
|
+
allFunding.push({
|
|
428
|
+
exchange: ex,
|
|
429
|
+
symbol: p.symbol,
|
|
430
|
+
payment: Number(p.payment),
|
|
431
|
+
time: p.time,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch { /* skip */ }
|
|
436
|
+
}));
|
|
437
|
+
// Aggregate
|
|
438
|
+
const bySymbol = new Map();
|
|
439
|
+
const byExchange = new Map();
|
|
440
|
+
let totalFunding = 0;
|
|
441
|
+
for (const f of allFunding) {
|
|
442
|
+
totalFunding += f.payment;
|
|
443
|
+
bySymbol.set(f.symbol, (bySymbol.get(f.symbol) ?? 0) + f.payment);
|
|
444
|
+
byExchange.set(f.exchange, (byExchange.get(f.exchange) ?? 0) + f.payment);
|
|
445
|
+
}
|
|
446
|
+
const result = {
|
|
447
|
+
totalPayments: allFunding.length,
|
|
448
|
+
totalFunding,
|
|
449
|
+
bySymbol: Object.fromEntries(bySymbol),
|
|
450
|
+
byExchange: Object.fromEntries(byExchange),
|
|
451
|
+
};
|
|
452
|
+
if (isJson())
|
|
453
|
+
return printJson(jsonOk(result));
|
|
454
|
+
console.log(chalk.cyan.bold("\n Funding Payment Summary\n"));
|
|
455
|
+
console.log(` Total Payments: ${allFunding.length}`);
|
|
456
|
+
console.log(` Net Funding: ${formatPnl(totalFunding)}`);
|
|
457
|
+
if (bySymbol.size > 0) {
|
|
458
|
+
console.log(chalk.white.bold("\n By Symbol:"));
|
|
459
|
+
const rows = [...bySymbol.entries()]
|
|
460
|
+
.sort((a, b) => b[1] - a[1])
|
|
461
|
+
.map(([sym, amt]) => [chalk.white.bold(sym), formatPnl(amt)]);
|
|
462
|
+
console.log(makeTable(["Symbol", "Net Funding"], rows));
|
|
463
|
+
}
|
|
464
|
+
if (byExchange.size > 0) {
|
|
465
|
+
console.log(chalk.white.bold(" By Exchange:"));
|
|
466
|
+
const rows = [...byExchange.entries()]
|
|
467
|
+
.map(([ex, amt]) => [chalk.white.bold(ex), formatPnl(amt)]);
|
|
468
|
+
console.log(makeTable(["Exchange", "Net Funding"], rows));
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
// ── analytics report ──
|
|
473
|
+
parent
|
|
474
|
+
.command("report")
|
|
475
|
+
.description("Full performance report (summary + pnl + funding)")
|
|
476
|
+
.option("-e, --exchange <exchanges>", "Comma-separated exchanges")
|
|
477
|
+
.option("--since <period>", "Period: 24h, 7d, 30d")
|
|
478
|
+
.action(async (opts) => {
|
|
479
|
+
await withJsonErrors(isJson(), async () => {
|
|
480
|
+
const since = parseSince(opts.since);
|
|
481
|
+
const stats = getExecutionStats(since);
|
|
482
|
+
const records = readExecutionLog({ since });
|
|
483
|
+
const exchanges = opts.exchange
|
|
484
|
+
? opts.exchange.split(",").map(e => e.trim())
|
|
485
|
+
: [...EXCHANGES];
|
|
486
|
+
// Volume from log
|
|
487
|
+
let logVolume = 0;
|
|
488
|
+
for (const r of records) {
|
|
489
|
+
if (r.notional)
|
|
490
|
+
logVolume += r.notional;
|
|
491
|
+
}
|
|
492
|
+
// Fetch live data from exchanges
|
|
493
|
+
let totalFunding = 0;
|
|
494
|
+
let totalFees = 0;
|
|
495
|
+
let totalEquity = 0;
|
|
496
|
+
let totalUPnl = 0;
|
|
497
|
+
const symbolSet = new Set();
|
|
498
|
+
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
499
|
+
try {
|
|
500
|
+
const adapter = await getAdapterForExchange(ex);
|
|
501
|
+
const [balance, trades, funding] = await Promise.all([
|
|
502
|
+
adapter.getBalance(),
|
|
503
|
+
adapter.getTradeHistory(50).catch(() => []),
|
|
504
|
+
adapter.getFundingPayments(50).catch(() => []),
|
|
505
|
+
]);
|
|
506
|
+
totalEquity += Number(balance.equity);
|
|
507
|
+
totalUPnl += Number(balance.unrealizedPnl);
|
|
508
|
+
for (const t of trades) {
|
|
509
|
+
totalFees += Number(t.fee);
|
|
510
|
+
symbolSet.add(t.symbol);
|
|
511
|
+
}
|
|
512
|
+
for (const f of funding)
|
|
513
|
+
totalFunding += Number(f.payment);
|
|
514
|
+
}
|
|
515
|
+
catch { /* skip */ }
|
|
516
|
+
}));
|
|
517
|
+
const report = {
|
|
518
|
+
period: opts.since ?? "all time",
|
|
519
|
+
execution: {
|
|
520
|
+
totalTrades: stats.totalTrades,
|
|
521
|
+
successRate: stats.successRate,
|
|
522
|
+
volume: logVolume,
|
|
523
|
+
byExchange: stats.byExchange,
|
|
524
|
+
byType: stats.byType,
|
|
525
|
+
},
|
|
526
|
+
portfolio: {
|
|
527
|
+
totalEquity,
|
|
528
|
+
unrealizedPnl: totalUPnl,
|
|
529
|
+
},
|
|
530
|
+
costs: {
|
|
531
|
+
totalFees,
|
|
532
|
+
totalFunding,
|
|
533
|
+
netFunding: totalFunding,
|
|
534
|
+
},
|
|
535
|
+
insights: {
|
|
536
|
+
uniqueSymbols: symbolSet.size,
|
|
537
|
+
mostActiveExchange: Object.entries(stats.byExchange).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "none",
|
|
538
|
+
avgTradesPerDay: stats.totalTrades > 0
|
|
539
|
+
? (stats.totalTrades / Math.max(1, (Date.now() - new Date(records[records.length - 1]?.timestamp ?? Date.now()).getTime()) / 86400000)).toFixed(1)
|
|
540
|
+
: "0",
|
|
541
|
+
},
|
|
542
|
+
};
|
|
543
|
+
if (isJson())
|
|
544
|
+
return printJson(jsonOk(report));
|
|
545
|
+
console.log(chalk.cyan.bold(`\n Performance Report ${opts.since ? `(${opts.since})` : ""}\n`));
|
|
546
|
+
console.log(chalk.white.bold(" Execution"));
|
|
547
|
+
console.log(` Trades: ${stats.totalTrades} (${stats.successRate.toFixed(0)}% success)`);
|
|
548
|
+
console.log(` Volume: $${formatUsd(logVolume)}`);
|
|
549
|
+
console.log(` Symbols Traded: ${symbolSet.size}`);
|
|
550
|
+
console.log(` Avg/Day: ${report.insights.avgTradesPerDay}`);
|
|
551
|
+
console.log(chalk.white.bold("\n Portfolio"));
|
|
552
|
+
console.log(` Total Equity: $${formatUsd(totalEquity)}`);
|
|
553
|
+
console.log(` Unrealized PnL: ${formatPnl(totalUPnl)}`);
|
|
554
|
+
console.log(chalk.white.bold("\n Costs & Income"));
|
|
555
|
+
console.log(` Trading Fees: ${chalk.red(`-$${formatUsd(totalFees)}`)}`);
|
|
556
|
+
console.log(` Funding Income: ${formatPnl(totalFunding)}`);
|
|
557
|
+
console.log(` Net: ${formatPnl(totalFunding - totalFees)}`);
|
|
558
|
+
console.log();
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
function parseSinceDate(since) {
|
|
563
|
+
if (!since)
|
|
564
|
+
return undefined;
|
|
565
|
+
const match = since.match(/^(\d+)(h|d|w)$/);
|
|
566
|
+
if (match) {
|
|
567
|
+
const [, num, unit] = match;
|
|
568
|
+
const ms = { h: 3600000, d: 86400000, w: 604800000 }[unit] ?? 86400000;
|
|
569
|
+
return new Date(Date.now() - parseInt(num) * ms);
|
|
570
|
+
}
|
|
571
|
+
return new Date(since);
|
|
572
|
+
}
|
|
573
|
+
/** Take equity snapshot for given adapters. */
|
|
574
|
+
async function takeSnapshot(getAdapterForExchange, exchanges) {
|
|
575
|
+
const ts = new Date().toISOString();
|
|
576
|
+
const snaps = [];
|
|
577
|
+
const results = await Promise.allSettled(exchanges.map(async (ex) => {
|
|
578
|
+
const adapter = await getAdapterForExchange(ex);
|
|
579
|
+
const [balance, positions] = await Promise.all([
|
|
580
|
+
adapter.getBalance(),
|
|
581
|
+
adapter.getPositions(),
|
|
582
|
+
]);
|
|
583
|
+
const snap = {
|
|
584
|
+
ts,
|
|
585
|
+
exchange: ex,
|
|
586
|
+
equity: Number(balance.equity),
|
|
587
|
+
available: Number(balance.available),
|
|
588
|
+
marginUsed: Number(balance.marginUsed),
|
|
589
|
+
unrealizedPnl: Number(balance.unrealizedPnl),
|
|
590
|
+
positionCount: positions.filter(p => Number(p.size) > 0).length,
|
|
591
|
+
};
|
|
592
|
+
saveEquitySnapshot(snap);
|
|
593
|
+
return snap;
|
|
594
|
+
}));
|
|
595
|
+
for (const r of results) {
|
|
596
|
+
if (r.status === "fulfilled")
|
|
597
|
+
snaps.push(r.value);
|
|
598
|
+
}
|
|
599
|
+
return snaps;
|
|
600
|
+
}
|
|
601
|
+
function registerPnlSubcommands(parent, getAdapterForExchange, isJson) {
|
|
602
|
+
// ── snapshot ──
|
|
603
|
+
parent
|
|
604
|
+
.command("snapshot")
|
|
605
|
+
.description("Take equity snapshot across all exchanges")
|
|
606
|
+
.option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
|
|
607
|
+
.action(async (opts) => {
|
|
608
|
+
await withJsonErrors(isJson(), async () => {
|
|
609
|
+
const exchanges = opts.exchanges.split(",").map(e => e.trim());
|
|
610
|
+
const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
|
|
611
|
+
if (isJson()) {
|
|
612
|
+
printJson(jsonOk({ snapshots: snaps }));
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
const total = snaps.reduce((s, sn) => s + sn.equity, 0);
|
|
616
|
+
console.log(chalk.cyan.bold("Equity Snapshot\n"));
|
|
617
|
+
for (const s of snaps) {
|
|
618
|
+
console.log(` ${s.exchange.padEnd(14)} $${formatUsd(s.equity)} ` +
|
|
619
|
+
`avail $${formatUsd(s.available)} ` +
|
|
620
|
+
`uPnL ${formatPnl(s.unrealizedPnl)} ` +
|
|
621
|
+
`pos: ${s.positionCount}`);
|
|
622
|
+
}
|
|
623
|
+
console.log(`\n ${"Total".padEnd(14)} $${formatUsd(total)}`);
|
|
624
|
+
console.log(chalk.gray(`\n Saved at ${snaps[0]?.ts ?? "—"}`));
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
// ── track ──
|
|
629
|
+
parent
|
|
630
|
+
.command("track")
|
|
631
|
+
.description("Continuously track equity (foreground)")
|
|
632
|
+
.option("--interval <sec>", "Snapshot interval in seconds", "300")
|
|
633
|
+
.option("--exchanges <list>", "Comma-separated exchange list", EXCHANGES.join(","))
|
|
634
|
+
.action(async (opts) => {
|
|
635
|
+
const intervalMs = parseInt(opts.interval) * 1000;
|
|
636
|
+
const exchanges = opts.exchanges.split(",").map(e => e.trim());
|
|
637
|
+
console.log(chalk.cyan(`PnL tracker started — snapshot every ${opts.interval}s for [${exchanges.join(", ")}]`));
|
|
638
|
+
console.log(chalk.gray("Press Ctrl+C to stop\n"));
|
|
639
|
+
const controller = new AbortController();
|
|
640
|
+
process.on("SIGINT", () => controller.abort());
|
|
641
|
+
// Initial snapshot
|
|
642
|
+
const snaps = await takeSnapshot(getAdapterForExchange, exchanges);
|
|
643
|
+
const total = snaps.reduce((s, sn) => s + sn.equity, 0);
|
|
644
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} Total equity: $${formatUsd(total)}`);
|
|
645
|
+
while (!controller.signal.aborted) {
|
|
646
|
+
await new Promise((resolve) => {
|
|
647
|
+
const timer = setTimeout(resolve, intervalMs);
|
|
648
|
+
controller.signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
649
|
+
});
|
|
650
|
+
if (controller.signal.aborted)
|
|
651
|
+
break;
|
|
652
|
+
try {
|
|
653
|
+
const s = await takeSnapshot(getAdapterForExchange, exchanges);
|
|
654
|
+
const t = s.reduce((sum, sn) => sum + sn.equity, 0);
|
|
655
|
+
const delta = t - total;
|
|
656
|
+
console.log(`${chalk.gray(new Date().toLocaleTimeString())} ` +
|
|
657
|
+
`Total: $${formatUsd(t)} ${formatPnl(delta)}`);
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
console.error(chalk.red(`Snapshot error: ${err instanceof Error ? err.message : err}`));
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
// ── perf (unified: daily/weekly/summary) ──
|
|
665
|
+
parent
|
|
666
|
+
.command("perf")
|
|
667
|
+
.description("Performance breakdown — daily, weekly, or summary stats")
|
|
668
|
+
.option("--period <period>", "daily, weekly, or summary", "daily")
|
|
669
|
+
.option("--exchange <name>", "Single exchange filter")
|
|
670
|
+
.option("--days <n>", "Number of days (daily mode)", "14")
|
|
671
|
+
.option("--weeks <n>", "Number of weeks (weekly mode)", "8")
|
|
672
|
+
.option("--since <period>", "Time range (e.g. 7d, 2w, 30d)")
|
|
673
|
+
.action(async (opts) => {
|
|
674
|
+
await withJsonErrors(isJson(), async () => {
|
|
675
|
+
const mode = opts.period.toLowerCase();
|
|
676
|
+
const defaultSince = mode === "weekly"
|
|
677
|
+
? new Date(Date.now() - parseInt(opts.weeks) * 7 * 86400000)
|
|
678
|
+
: new Date(Date.now() - parseInt(opts.days) * 86400000);
|
|
679
|
+
const since = parseSinceDate(opts.since) ?? defaultSince;
|
|
680
|
+
const snapshots = readEquityHistory({ exchange: opts.exchange, since });
|
|
681
|
+
if (snapshots.length === 0) {
|
|
682
|
+
if (isJson()) {
|
|
683
|
+
printJson(jsonError("NO_DATA", "No equity snapshots found. Run 'perp history snapshot' or 'perp history track' first."));
|
|
684
|
+
}
|
|
685
|
+
else {
|
|
686
|
+
console.log(chalk.yellow("No equity snapshots found. Run 'perp history snapshot' or 'perp history track' first."));
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (mode === "summary") {
|
|
691
|
+
const metrics = computePnlMetrics(snapshots, opts.exchange);
|
|
692
|
+
if (isJson()) {
|
|
693
|
+
printJson(jsonOk({ ...metrics, dailyReturns: undefined, dailyCount: metrics.dailyReturns.length }));
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
console.log(chalk.cyan.bold(`Performance Summary${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
|
|
697
|
+
console.log(` Period: ${metrics.period.from} → ${metrics.period.to} (${metrics.period.days} days)`);
|
|
698
|
+
console.log(` Total Return: ${formatPnl(metrics.totalReturn)} (${metrics.totalReturnPct >= 0 ? "+" : ""}${metrics.totalReturnPct.toFixed(2)}%)`);
|
|
699
|
+
console.log(` Peak Equity: $${formatUsd(metrics.peakEquity)}`);
|
|
700
|
+
console.log(` Avg Daily PnL: ${formatPnl(metrics.avgDailyPnl)}`);
|
|
701
|
+
console.log();
|
|
702
|
+
console.log(` Sharpe Ratio: ${metrics.sharpeRatio >= 0 ? chalk.green(metrics.sharpeRatio.toFixed(2)) : chalk.red(metrics.sharpeRatio.toFixed(2))}`);
|
|
703
|
+
console.log(` Max Drawdown: ${chalk.red(`$${formatUsd(metrics.maxDrawdown)}`)} (${chalk.red(`-${metrics.maxDrawdownPct.toFixed(2)}%`)})`);
|
|
704
|
+
console.log(` Current DD: $${formatUsd(metrics.currentDrawdown)} (-${metrics.currentDrawdownPct.toFixed(2)}%)`);
|
|
705
|
+
console.log();
|
|
706
|
+
console.log(` Win Days: ${chalk.green(String(metrics.winDays))} / ${metrics.winDays + metrics.lossDays}`);
|
|
707
|
+
console.log(` Win Rate: ${metrics.winRate.toFixed(1)}%`);
|
|
708
|
+
if (metrics.bestDay)
|
|
709
|
+
console.log(` Best Day: ${metrics.bestDay.date} ${formatPnl(metrics.bestDay.pnl)}`);
|
|
710
|
+
if (metrics.worstDay)
|
|
711
|
+
console.log(` Worst Day: ${metrics.worstDay.date} ${formatPnl(metrics.worstDay.pnl)}`);
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const daily = computeDailyPnl(snapshots, opts.exchange);
|
|
716
|
+
if (mode === "weekly") {
|
|
717
|
+
const weekly = aggregateWeekly(daily);
|
|
718
|
+
if (isJson()) {
|
|
719
|
+
printJson(jsonOk({ weekly }));
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
console.log(chalk.cyan.bold(`Weekly PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
|
|
723
|
+
const rows = weekly.map(w => [w.date, `$${formatUsd(w.startEquity)}`, `$${formatUsd(w.endEquity)}`, formatPnl(w.pnl), `${w.pnlPct >= 0 ? "+" : ""}${w.pnlPct.toFixed(2)}%`]);
|
|
724
|
+
console.log(makeTable(["Week", "Start", "End", "PnL", "PnL %"], rows));
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
// daily (default)
|
|
728
|
+
if (isJson()) {
|
|
729
|
+
printJson(jsonOk({ daily }));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
console.log(chalk.cyan.bold(`Daily PnL${opts.exchange ? ` (${opts.exchange})` : ""}\n`));
|
|
733
|
+
const rows = daily.map(d => [d.date, `$${formatUsd(d.startEquity)}`, `$${formatUsd(d.endEquity)}`, formatPnl(d.pnl), `${d.pnlPct >= 0 ? "+" : ""}${d.pnlPct.toFixed(2)}%`]);
|
|
734
|
+
console.log(makeTable(["Date", "Start", "End", "PnL", "PnL %"], rows));
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
235
738
|
}
|
package/dist/commands/manage.js
CHANGED
|
@@ -3,7 +3,7 @@ import { printJson, jsonOk } from "../utils.js";
|
|
|
3
3
|
import { setEnvVar } from "./init.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
export function registerManageCommands(program, getAdapter, isJson, getPacificaAdapter) {
|
|
6
|
-
const manage = program.command("manage").description("
|
|
6
|
+
const manage = program.command("manage").description("Exchange account settings (margin, subaccount, API keys, builder)");
|
|
7
7
|
// Ensure adapter is initialized before accessing PacificaAdapter
|
|
8
8
|
async function pac() {
|
|
9
9
|
const adapter = await getAdapter();
|
|
@@ -27,16 +27,18 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
|
|
|
27
27
|
return printJson(jsonOk(result));
|
|
28
28
|
console.log(chalk.green(`\n Margin mode for ${symbol.toUpperCase()} set to ${m}.\n`));
|
|
29
29
|
});
|
|
30
|
-
manage
|
|
30
|
+
const withdrawCmd = manage
|
|
31
31
|
.command("withdraw <amount> <address>")
|
|
32
|
-
.description("
|
|
32
|
+
.description("Use 'perp withdraw pacifica <amount>'")
|
|
33
33
|
.action(async (amount, address) => {
|
|
34
|
+
console.log(chalk.yellow("\n Use 'perp withdraw pacifica <amount>' instead.\n"));
|
|
34
35
|
const a = await pac();
|
|
35
36
|
const result = await a.sdk.withdraw({ amount, dest_address: address }, a.publicKey, a.signer);
|
|
36
37
|
if (isJson())
|
|
37
38
|
return printJson(jsonOk(result));
|
|
38
39
|
console.log(chalk.green(`\n Withdrawal of $${amount} to ${address} submitted.\n`));
|
|
39
40
|
});
|
|
41
|
+
withdrawCmd._hidden = true;
|
|
40
42
|
// Subaccounts
|
|
41
43
|
const sub = manage.command("sub").description("Subaccount management");
|
|
42
44
|
sub
|
|
@@ -274,7 +276,7 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
|
|
|
274
276
|
manage
|
|
275
277
|
.command("setup-api-key")
|
|
276
278
|
.description("Generate & register a Lighter API key (required for trading)")
|
|
277
|
-
.option("--key-index <n>", "API key index (
|
|
279
|
+
.option("--key-index <n>", "API key index (4-254, default: 4)", "4")
|
|
278
280
|
.action(async (opts) => {
|
|
279
281
|
const adapter = await getAdapter();
|
|
280
282
|
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
package/dist/commands/market.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PacificaAdapter } from "../exchanges/pacifica.js";
|
|
2
|
-
import {
|
|
2
|
+
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
3
|
+
import { makeTable, formatUsd, formatPercent, printJson, jsonOk, jsonError, withJsonErrors } from "../utils.js";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
export function registerMarketCommands(program, getAdapter, isJson) {
|
|
5
6
|
const market = program.command("market").description("Market data commands");
|
|
@@ -139,7 +140,7 @@ export function registerMarketCommands(program, getAdapter, isJson) {
|
|
|
139
140
|
const rows = history.map((h) => [
|
|
140
141
|
new Date(h.time).toLocaleString(),
|
|
141
142
|
formatPercent(h.rate),
|
|
142
|
-
`$${formatUsd(h.price)}
|
|
143
|
+
h.price != null ? `$${formatUsd(h.price)}` : chalk.gray("n/a"),
|
|
143
144
|
]);
|
|
144
145
|
console.log(makeTable(["Time", "Funding Rate", "Oracle"], rows));
|
|
145
146
|
});
|
|
@@ -222,4 +223,36 @@ export function registerMarketCommands(program, getAdapter, isJson) {
|
|
|
222
223
|
]);
|
|
223
224
|
console.log(makeTable(["Time", "Open", "High", "Low", "Close", "Volume", "Trades"], rows));
|
|
224
225
|
});
|
|
226
|
+
// ── market hip3 ── (HIP-3 deployed perp dexes on Hyperliquid)
|
|
227
|
+
market
|
|
228
|
+
.command("hip3")
|
|
229
|
+
.description("List HIP-3 deployed perp dexes (Hyperliquid only)")
|
|
230
|
+
.action(async () => {
|
|
231
|
+
await withJsonErrors(isJson(), async () => {
|
|
232
|
+
const adapter = await getAdapter();
|
|
233
|
+
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
234
|
+
if (isJson())
|
|
235
|
+
return printJson(jsonError("INVALID_EXCHANGE", "HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid."));
|
|
236
|
+
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const dexes = await adapter.listDeployedDexes();
|
|
240
|
+
if (isJson())
|
|
241
|
+
return printJson(jsonOk(dexes));
|
|
242
|
+
if (dexes.length === 0) {
|
|
243
|
+
console.log(chalk.gray("\n No deployed dexes found.\n"));
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
console.log(chalk.cyan.bold("\n HIP-3 Deployed Perp DEXes\n"));
|
|
247
|
+
const rows = dexes.map(d => [
|
|
248
|
+
chalk.white.bold(d.name),
|
|
249
|
+
chalk.gray(d.deployer.slice(0, 10) + "..."),
|
|
250
|
+
String(d.assets.length),
|
|
251
|
+
d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
|
|
252
|
+
]);
|
|
253
|
+
console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
|
|
254
|
+
console.log(chalk.gray(`\n Use --dex <name> to trade on a deployed dex.`));
|
|
255
|
+
console.log(chalk.gray(` Example: perp -e hl --dex xyz market list\n`));
|
|
256
|
+
});
|
|
257
|
+
});
|
|
225
258
|
}
|
|
@@ -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;
|