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
package/dist/commands/bot.js
CHANGED
|
@@ -3,6 +3,12 @@ import { printJson, jsonOk } from "../utils.js";
|
|
|
3
3
|
import { loadBotConfig, quickGridConfig, quickDCAConfig } from "../bot/config.js";
|
|
4
4
|
import { runBot } from "../bot/engine.js";
|
|
5
5
|
import { PRESETS, getPreset, getPresetsByStrategy } from "../bot/presets.js";
|
|
6
|
+
import { updateJobState } from "../jobs.js";
|
|
7
|
+
import { runTWAP } from "../strategies/twap.js";
|
|
8
|
+
import { runFundingArb } from "../strategies/funding-arb.js";
|
|
9
|
+
import { runGrid } from "../strategies/grid.js";
|
|
10
|
+
import { runDCA } from "../strategies/dca.js";
|
|
11
|
+
import { runTrailingStop } from "../strategies/trailing-stop.js";
|
|
6
12
|
export function registerBotCommands(program, getAdapter, getAdapterFor, isJson) {
|
|
7
13
|
const bot = program.command("bot").description("Automated trading bots with condition monitoring & risk management");
|
|
8
14
|
// ── bot start <config> ──
|
|
@@ -326,6 +332,8 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
|
|
|
326
332
|
console.log(chalk.gray(` Usage: perp bot start ~/.perp/bots/eth-grid.yaml`));
|
|
327
333
|
console.log(chalk.gray(` perp bot start ~/.perp/bots/eth-grid.yaml --background\n`));
|
|
328
334
|
});
|
|
335
|
+
// Strategy subcommands: twap, funding-arb, grid, dca, trailing-stop
|
|
336
|
+
registerRunSubcommands(bot, getAdapter, getAdapterFor, isJson);
|
|
329
337
|
}
|
|
330
338
|
function makeLog() {
|
|
331
339
|
return (msg) => {
|
|
@@ -410,3 +418,202 @@ const ARB_EXAMPLE = chalk.gray(` name: funding-arb
|
|
|
410
418
|
risk:
|
|
411
419
|
max_drawdown: 200
|
|
412
420
|
max_daily_loss: 50`);
|
|
421
|
+
// ── Run subcommands (twap, funding-arb, grid, dca, trailing-stop) ──
|
|
422
|
+
function registerRunSubcommands(parent, getAdapter, getAdapterFor, isJson) {
|
|
423
|
+
const run = parent;
|
|
424
|
+
const twapCmd = run
|
|
425
|
+
.command("twap <symbol> <side> <size> <duration>")
|
|
426
|
+
.description("Run client-side TWAP (splits market orders over time)");
|
|
427
|
+
twapCmd
|
|
428
|
+
.option("-s, --slices <n>", "Number of slices (default: auto)")
|
|
429
|
+
.option("--job-id <id>", "Job ID (set automatically for background jobs)")
|
|
430
|
+
.action(async (symbol, side, size, duration, opts) => {
|
|
431
|
+
const s = side.toLowerCase();
|
|
432
|
+
if (s !== "buy" && s !== "sell") {
|
|
433
|
+
console.error(chalk.red("Side must be buy or sell"));
|
|
434
|
+
process.exit(1);
|
|
435
|
+
}
|
|
436
|
+
const adapter = await getAdapter();
|
|
437
|
+
const params = {
|
|
438
|
+
symbol: symbol.toUpperCase(),
|
|
439
|
+
side: s,
|
|
440
|
+
totalSize: parseFloat(size),
|
|
441
|
+
durationSec: parseInt(duration),
|
|
442
|
+
slices: opts.slices ? parseInt(opts.slices) : undefined,
|
|
443
|
+
};
|
|
444
|
+
const log = (msg) => {
|
|
445
|
+
const ts = new Date().toLocaleTimeString();
|
|
446
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
447
|
+
};
|
|
448
|
+
const result = await runTWAP(adapter, params, opts.jobId, log);
|
|
449
|
+
if (isJson())
|
|
450
|
+
printJson(jsonOk(result));
|
|
451
|
+
if (opts.jobId) {
|
|
452
|
+
updateJobState(opts.jobId, { status: "done" });
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
run
|
|
456
|
+
.command("funding-arb")
|
|
457
|
+
.description("Run funding rate arbitrage monitor/executor")
|
|
458
|
+
.option("--min-spread <pct>", "Min annual spread % to enter", "10")
|
|
459
|
+
.option("--close-spread <pct>", "Close when spread drops below (%)", "5")
|
|
460
|
+
.option("--size <size>", "Position size per leg (base amount)", "0.01")
|
|
461
|
+
.option("--size-usd <usd>", "Position size per leg in USD (alternative)")
|
|
462
|
+
.option("--symbols <list>", "Comma-separated symbol filter")
|
|
463
|
+
.option("--interval <sec>", "Check interval in seconds", "60")
|
|
464
|
+
.option("--auto-execute", "Actually place trades (default: monitor only)")
|
|
465
|
+
.option("--max-positions <n>", "Max simultaneous positions", "3")
|
|
466
|
+
.option("--exchanges <list>", "Comma-separated exchanges to use", "pacifica,hyperliquid,lighter")
|
|
467
|
+
.option("--auto-rebalance", "Trigger rebalance alerts when exchange balance is low")
|
|
468
|
+
.option("--rebalance-threshold <usd>", "Rebalance when available < this ($)", "100")
|
|
469
|
+
.option("--max-drawdown <usd>", "Close all positions if total uPnL exceeds this loss ($)")
|
|
470
|
+
.option("--job-id <id>", "Job ID (set automatically for background jobs)")
|
|
471
|
+
.action(async (opts) => {
|
|
472
|
+
const exchangeNames = opts.exchanges.split(",").map((e) => e.trim());
|
|
473
|
+
const adapters = new Map();
|
|
474
|
+
for (const name of exchangeNames) {
|
|
475
|
+
try {
|
|
476
|
+
const a = await getAdapterFor(name);
|
|
477
|
+
adapters.set(name, a);
|
|
478
|
+
}
|
|
479
|
+
catch {
|
|
480
|
+
if (!isJson())
|
|
481
|
+
console.log(chalk.yellow(` Skipping ${name} (no credentials)`));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (adapters.size < 2) {
|
|
485
|
+
console.error(chalk.red("Need at least 2 exchanges for arbitrage. Set credentials for 2+ exchanges."));
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
const params = {
|
|
489
|
+
minSpread: parseFloat(opts.minSpread),
|
|
490
|
+
closeSpread: parseFloat(opts.closeSpread),
|
|
491
|
+
size: opts.size,
|
|
492
|
+
sizeUsd: opts.sizeUsd ? parseFloat(opts.sizeUsd) : undefined,
|
|
493
|
+
symbols: opts.symbols ? opts.symbols.split(",").map((s) => s.trim().toUpperCase()) : undefined,
|
|
494
|
+
intervalSec: parseInt(opts.interval),
|
|
495
|
+
autoExecute: opts.autoExecute ?? false,
|
|
496
|
+
maxPositions: parseInt(opts.maxPositions),
|
|
497
|
+
autoRebalance: opts.autoRebalance ?? false,
|
|
498
|
+
rebalanceThreshold: parseFloat(opts.rebalanceThreshold),
|
|
499
|
+
maxDrawdown: opts.maxDrawdown ? parseFloat(opts.maxDrawdown) : undefined,
|
|
500
|
+
};
|
|
501
|
+
const log = (msg) => {
|
|
502
|
+
const ts = new Date().toLocaleTimeString();
|
|
503
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
504
|
+
};
|
|
505
|
+
await runFundingArb(adapters, params, opts.jobId, log);
|
|
506
|
+
});
|
|
507
|
+
// ── Grid Bot ──
|
|
508
|
+
const gridCmd = run
|
|
509
|
+
.command("grid <symbol>")
|
|
510
|
+
.description("Run grid trading bot (places limit orders across a price range)");
|
|
511
|
+
gridCmd
|
|
512
|
+
.requiredOption("--upper <price>", "Upper price bound")
|
|
513
|
+
.requiredOption("--lower <price>", "Lower price bound")
|
|
514
|
+
.option("--grids <n>", "Number of grid lines", "10")
|
|
515
|
+
.option("--size <size>", "Total position size (base)", "0.1")
|
|
516
|
+
.option("--side <side>", "Grid bias: long, short, neutral", "neutral")
|
|
517
|
+
.option("--leverage <n>", "Leverage to set")
|
|
518
|
+
.option("--interval <sec>", "Check interval in seconds", "10")
|
|
519
|
+
.option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
|
|
520
|
+
.option("--trailing-stop <pct>", "Stop if equity drops by this % from peak")
|
|
521
|
+
.option("--job-id <id>", "Job ID (set automatically for background jobs)")
|
|
522
|
+
.action(async (symbol, opts) => {
|
|
523
|
+
const side = opts.side.toLowerCase();
|
|
524
|
+
if (side !== "long" && side !== "short" && side !== "neutral") {
|
|
525
|
+
console.error(chalk.red("Side must be long, short, or neutral"));
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
const adapter = await getAdapter();
|
|
529
|
+
const params = {
|
|
530
|
+
symbol: symbol.toUpperCase(),
|
|
531
|
+
side: side,
|
|
532
|
+
upperPrice: parseFloat(opts.upper),
|
|
533
|
+
lowerPrice: parseFloat(opts.lower),
|
|
534
|
+
grids: parseInt(opts.grids),
|
|
535
|
+
totalSize: parseFloat(opts.size),
|
|
536
|
+
leverage: opts.leverage ? parseInt(opts.leverage) : undefined,
|
|
537
|
+
intervalSec: parseInt(opts.interval),
|
|
538
|
+
maxRuntime: parseInt(opts.maxRuntime),
|
|
539
|
+
trailingStop: opts.trailingStop ? parseFloat(opts.trailingStop) : undefined,
|
|
540
|
+
};
|
|
541
|
+
const log = (msg) => {
|
|
542
|
+
const ts = new Date().toLocaleTimeString();
|
|
543
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
544
|
+
};
|
|
545
|
+
const result = await runGrid(adapter, params, opts.jobId, log);
|
|
546
|
+
if (isJson())
|
|
547
|
+
printJson(jsonOk(result));
|
|
548
|
+
if (opts.jobId) {
|
|
549
|
+
updateJobState(opts.jobId, { status: "done" });
|
|
550
|
+
}
|
|
551
|
+
});
|
|
552
|
+
// ── DCA (Dollar Cost Averaging) ──
|
|
553
|
+
const dcaCmd = run
|
|
554
|
+
.command("dca <symbol> <side> <amount> <interval>")
|
|
555
|
+
.description("Run DCA strategy (periodic market orders at fixed intervals)");
|
|
556
|
+
dcaCmd
|
|
557
|
+
.option("--orders <n>", "Total number of orders (0 = unlimited)", "0")
|
|
558
|
+
.option("--price-limit <price>", "Stop buying above / selling below this price")
|
|
559
|
+
.option("--max-runtime <sec>", "Max runtime in seconds (0 = forever)", "0")
|
|
560
|
+
.option("--job-id <id>", "Job ID (set automatically for background jobs)")
|
|
561
|
+
.action(async (symbol, side, amount, interval, opts) => {
|
|
562
|
+
const s = side.toLowerCase();
|
|
563
|
+
if (s !== "buy" && s !== "sell") {
|
|
564
|
+
console.error(chalk.red("Side must be buy or sell"));
|
|
565
|
+
process.exit(1);
|
|
566
|
+
}
|
|
567
|
+
const adapter = await getAdapter();
|
|
568
|
+
const params = {
|
|
569
|
+
symbol: symbol.toUpperCase(),
|
|
570
|
+
side: s,
|
|
571
|
+
amountPerOrder: parseFloat(amount),
|
|
572
|
+
intervalSec: parseInt(interval),
|
|
573
|
+
totalOrders: parseInt(opts.orders),
|
|
574
|
+
priceLimit: opts.priceLimit ? parseFloat(opts.priceLimit) : undefined,
|
|
575
|
+
maxRuntime: parseInt(opts.maxRuntime),
|
|
576
|
+
};
|
|
577
|
+
const log = (msg) => {
|
|
578
|
+
const ts = new Date().toLocaleTimeString();
|
|
579
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
580
|
+
};
|
|
581
|
+
const result = await runDCA(adapter, params, opts.jobId, log);
|
|
582
|
+
if (isJson())
|
|
583
|
+
printJson(jsonOk(result));
|
|
584
|
+
if (opts.jobId) {
|
|
585
|
+
updateJobState(opts.jobId, { status: "done" });
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
// ── Trailing Stop ──
|
|
589
|
+
const tsCmd = run
|
|
590
|
+
.command("trailing-stop <symbol>")
|
|
591
|
+
.description("Run client-side trailing stop (monitors price, closes position when trail % hit)");
|
|
592
|
+
tsCmd
|
|
593
|
+
.requiredOption("--trail <pct>", "Trail percentage")
|
|
594
|
+
.option("--interval <sec>", "Check interval in seconds", "5")
|
|
595
|
+
.option("--activation <price>", "Only start trailing after price reaches this level")
|
|
596
|
+
.option("--job-id <id>", "Job ID (set automatically for background jobs)")
|
|
597
|
+
.action(async (symbol, opts) => {
|
|
598
|
+
const sym = symbol.toUpperCase();
|
|
599
|
+
const trailPct = parseFloat(opts.trail);
|
|
600
|
+
const intervalSec = parseInt(opts.interval);
|
|
601
|
+
const activationPrice = opts.activation ? parseFloat(opts.activation) : undefined;
|
|
602
|
+
const adapter = await getAdapter();
|
|
603
|
+
const log = (msg) => {
|
|
604
|
+
const ts = new Date().toLocaleTimeString();
|
|
605
|
+
console.log(`${chalk.gray(ts)} ${msg}`);
|
|
606
|
+
};
|
|
607
|
+
const result = await runTrailingStop(adapter, {
|
|
608
|
+
symbol: sym,
|
|
609
|
+
trailPct,
|
|
610
|
+
intervalSec,
|
|
611
|
+
activationPrice,
|
|
612
|
+
}, opts.jobId, log);
|
|
613
|
+
if (isJson())
|
|
614
|
+
printJson(jsonOk(result));
|
|
615
|
+
if (opts.jobId) {
|
|
616
|
+
updateJobState(opts.jobId, { status: "done" });
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
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) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import type { ExchangeAdapter } from "../exchanges/interface.js";
|
|
3
|
+
import type { Network } from "../pacifica/index.js";
|
|
4
|
+
export declare function registerFundsCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, getNetwork: () => Network): void;
|