perp-cli 0.3.3
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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/__tests__/alert-logic.test.d.ts +1 -0
- package/dist/__tests__/alert-logic.test.js +107 -0
- package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
- package/dist/__tests__/arb-auto-3dex.test.js +397 -0
- package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
- package/dist/__tests__/arb-history-stats.test.js +176 -0
- package/dist/__tests__/arb-logic.test.d.ts +1 -0
- package/dist/__tests__/arb-logic.test.js +84 -0
- package/dist/__tests__/arb-manage.test.d.ts +1 -0
- package/dist/__tests__/arb-manage.test.js +253 -0
- package/dist/__tests__/arb-new-features.test.d.ts +1 -0
- package/dist/__tests__/arb-new-features.test.js +457 -0
- package/dist/__tests__/arb-sizing.test.d.ts +1 -0
- package/dist/__tests__/arb-sizing.test.js +48 -0
- package/dist/__tests__/arb-state.test.d.ts +1 -0
- package/dist/__tests__/arb-state.test.js +284 -0
- package/dist/__tests__/arb-userflow.test.d.ts +1 -0
- package/dist/__tests__/arb-userflow.test.js +945 -0
- package/dist/__tests__/arb-utils.test.d.ts +1 -0
- package/dist/__tests__/arb-utils.test.js +264 -0
- package/dist/__tests__/bot-conditions.test.d.ts +1 -0
- package/dist/__tests__/bot-conditions.test.js +341 -0
- package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
- package/dist/__tests__/client-id-tracker.test.js +137 -0
- package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
- package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
- package/dist/__tests__/commands/order-intent.test.js +600 -0
- package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
- package/dist/__tests__/commands/trade-commands.test.js +821 -0
- package/dist/__tests__/config.test.d.ts +1 -0
- package/dist/__tests__/config.test.js +86 -0
- package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
- package/dist/__tests__/cross-chain-margin.test.js +287 -0
- package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
- package/dist/__tests__/dex-asset-map.test.js +191 -0
- package/dist/__tests__/errors.test.d.ts +1 -0
- package/dist/__tests__/errors.test.js +110 -0
- package/dist/__tests__/event-stream.test.d.ts +1 -0
- package/dist/__tests__/event-stream.test.js +276 -0
- package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
- package/dist/__tests__/exchanges/interface.test.js +132 -0
- package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
- package/dist/__tests__/exchanges/mock-adapter.js +137 -0
- package/dist/__tests__/execution-log.test.d.ts +1 -0
- package/dist/__tests__/execution-log.test.js +106 -0
- package/dist/__tests__/funding-calc.test.d.ts +1 -0
- package/dist/__tests__/funding-calc.test.js +71 -0
- package/dist/__tests__/funding-history.test.d.ts +1 -0
- package/dist/__tests__/funding-history.test.js +343 -0
- package/dist/__tests__/funding-rates.test.d.ts +1 -0
- package/dist/__tests__/funding-rates.test.js +342 -0
- package/dist/__tests__/funding.test.d.ts +1 -0
- package/dist/__tests__/funding.test.js +173 -0
- package/dist/__tests__/gap-logic.test.d.ts +1 -0
- package/dist/__tests__/gap-logic.test.js +43 -0
- package/dist/__tests__/hip3-dex.test.d.ts +1 -0
- package/dist/__tests__/hip3-dex.test.js +234 -0
- package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
- package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/bridge.integration.test.js +309 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
- package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
- package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/lighter.integration.test.js +53 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
- package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
- package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
- package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
- package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
- package/dist/__tests__/liquidity.test.d.ts +1 -0
- package/dist/__tests__/liquidity.test.js +225 -0
- package/dist/__tests__/plan-executor.test.d.ts +1 -0
- package/dist/__tests__/plan-executor.test.js +314 -0
- package/dist/__tests__/position-history.test.d.ts +1 -0
- package/dist/__tests__/position-history.test.js +367 -0
- package/dist/__tests__/retry.test.d.ts +1 -0
- package/dist/__tests__/retry.test.js +310 -0
- package/dist/__tests__/risk-assessment.test.d.ts +1 -0
- package/dist/__tests__/risk-assessment.test.js +145 -0
- package/dist/__tests__/security-adversarial.test.d.ts +1 -0
- package/dist/__tests__/security-adversarial.test.js +574 -0
- package/dist/__tests__/strategies.test.d.ts +1 -0
- package/dist/__tests__/strategies.test.js +539 -0
- package/dist/__tests__/trade-execution.test.d.ts +1 -0
- package/dist/__tests__/trade-execution.test.js +129 -0
- package/dist/__tests__/trade-validator.test.d.ts +1 -0
- package/dist/__tests__/trade-validator.test.js +655 -0
- package/dist/__tests__/utils.test.d.ts +1 -0
- package/dist/__tests__/utils.test.js +76 -0
- package/dist/api/public/hyperliquid.d.ts +18 -0
- package/dist/api/public/hyperliquid.js +82 -0
- package/dist/api/public/index.d.ts +8 -0
- package/dist/api/public/index.js +8 -0
- package/dist/api/public/lighter.d.ts +24 -0
- package/dist/api/public/lighter.js +100 -0
- package/dist/api/public/pacifica.d.ts +17 -0
- package/dist/api/public/pacifica.js +54 -0
- package/dist/api/public/urls.d.ts +12 -0
- package/dist/api/public/urls.js +33 -0
- package/dist/arb/history-stats.d.ts +44 -0
- package/dist/arb/history-stats.js +135 -0
- package/dist/arb/index.d.ts +4 -0
- package/dist/arb/index.js +4 -0
- package/dist/arb/sizing.d.ts +23 -0
- package/dist/arb/sizing.js +96 -0
- package/dist/arb/state.d.ts +51 -0
- package/dist/arb/state.js +112 -0
- package/dist/arb/utils.d.ts +81 -0
- package/dist/arb/utils.js +267 -0
- package/dist/arb-history-stats.d.ts +5 -0
- package/dist/arb-history-stats.js +5 -0
- package/dist/arb-sizing.d.ts +5 -0
- package/dist/arb-sizing.js +5 -0
- package/dist/arb-state.d.ts +5 -0
- package/dist/arb-state.js +5 -0
- package/dist/arb-utils.d.ts +5 -0
- package/dist/arb-utils.js +5 -0
- package/dist/bot/conditions.d.ts +32 -0
- package/dist/bot/conditions.js +141 -0
- package/dist/bot/config.d.ts +76 -0
- package/dist/bot/config.js +160 -0
- package/dist/bot/engine.d.ts +8 -0
- package/dist/bot/engine.js +519 -0
- package/dist/bot/presets.d.ts +11 -0
- package/dist/bot/presets.js +296 -0
- package/dist/bridge-engine.d.ts +133 -0
- package/dist/bridge-engine.js +1487 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.js +99 -0
- package/dist/cli-spec.d.ts +50 -0
- package/dist/cli-spec.js +75 -0
- package/dist/client-id-tracker.d.ts +25 -0
- package/dist/client-id-tracker.js +76 -0
- package/dist/commands/account.d.ts +3 -0
- package/dist/commands/account.js +425 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +386 -0
- package/dist/commands/alert.d.ts +2 -0
- package/dist/commands/alert.js +421 -0
- package/dist/commands/analytics.d.ts +3 -0
- package/dist/commands/analytics.js +311 -0
- package/dist/commands/arb/index.d.ts +3 -0
- package/dist/commands/arb/index.js +921 -0
- package/dist/commands/arb-auto.d.ts +54 -0
- package/dist/commands/arb-auto.js +1328 -0
- package/dist/commands/arb-manage.d.ts +5 -0
- package/dist/commands/arb-manage.js +5 -0
- package/dist/commands/arb.d.ts +2 -0
- package/dist/commands/arb.js +347 -0
- package/dist/commands/backtest.d.ts +2 -0
- package/dist/commands/backtest.js +327 -0
- package/dist/commands/bot.d.ts +3 -0
- package/dist/commands/bot.js +412 -0
- package/dist/commands/bridge.d.ts +2 -0
- package/dist/commands/bridge.js +396 -0
- package/dist/commands/dashboard.d.ts +3 -0
- package/dist/commands/dashboard.js +176 -0
- package/dist/commands/deposit.d.ts +4 -0
- package/dist/commands/deposit.js +573 -0
- package/dist/commands/dex.d.ts +3 -0
- package/dist/commands/dex.js +114 -0
- package/dist/commands/env.d.ts +2 -0
- package/dist/commands/env.js +136 -0
- package/dist/commands/funding.d.ts +2 -0
- package/dist/commands/funding.js +347 -0
- package/dist/commands/gap.d.ts +2 -0
- package/dist/commands/gap.js +305 -0
- package/dist/commands/health.d.ts +2 -0
- package/dist/commands/health.js +67 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +235 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +266 -0
- package/dist/commands/jobs.d.ts +2 -0
- package/dist/commands/jobs.js +133 -0
- package/dist/commands/manage.d.ts +4 -0
- package/dist/commands/manage.js +309 -0
- package/dist/commands/market.d.ts +3 -0
- package/dist/commands/market.js +225 -0
- package/dist/commands/plan.d.ts +3 -0
- package/dist/commands/plan.js +95 -0
- package/dist/commands/portfolio.d.ts +3 -0
- package/dist/commands/portfolio.js +169 -0
- package/dist/commands/rebalance.d.ts +3 -0
- package/dist/commands/rebalance.js +293 -0
- package/dist/commands/risk.d.ts +3 -0
- package/dist/commands/risk.js +169 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +202 -0
- package/dist/commands/settings.d.ts +2 -0
- package/dist/commands/settings.js +102 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +123 -0
- package/dist/commands/trade.d.ts +3 -0
- package/dist/commands/trade.js +1273 -0
- package/dist/commands/wallet.d.ts +14 -0
- package/dist/commands/wallet.js +602 -0
- package/dist/commands/withdraw.d.ts +3 -0
- package/dist/commands/withdraw.js +187 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +68 -0
- package/dist/cross-chain-margin.d.ts +46 -0
- package/dist/cross-chain-margin.js +107 -0
- package/dist/dashboard/server.d.ts +80 -0
- package/dist/dashboard/server.js +340 -0
- package/dist/dashboard/ui.d.ts +4 -0
- package/dist/dashboard/ui.js +538 -0
- package/dist/dashboard/ws-feeds.d.ts +29 -0
- package/dist/dashboard/ws-feeds.js +660 -0
- package/dist/dex-asset-map.d.ts +80 -0
- package/dist/dex-asset-map.js +201 -0
- package/dist/errors.d.ts +109 -0
- package/dist/errors.js +84 -0
- package/dist/event-stream.d.ts +25 -0
- package/dist/event-stream.js +168 -0
- package/dist/exchanges/hyperliquid.d.ts +212 -0
- package/dist/exchanges/hyperliquid.js +931 -0
- package/dist/exchanges/interface.d.ts +95 -0
- package/dist/exchanges/interface.js +5 -0
- package/dist/exchanges/lighter.d.ts +159 -0
- package/dist/exchanges/lighter.js +793 -0
- package/dist/exchanges/pacifica.d.ts +51 -0
- package/dist/exchanges/pacifica.js +248 -0
- package/dist/execution-log.d.ts +36 -0
- package/dist/execution-log.js +102 -0
- package/dist/funding/history.d.ts +63 -0
- package/dist/funding/history.js +266 -0
- package/dist/funding/index.d.ts +3 -0
- package/dist/funding/index.js +3 -0
- package/dist/funding/normalize.d.ts +39 -0
- package/dist/funding/normalize.js +66 -0
- package/dist/funding/rates.d.ts +45 -0
- package/dist/funding/rates.js +172 -0
- package/dist/funding-history.d.ts +5 -0
- package/dist/funding-history.js +5 -0
- package/dist/funding-rates.d.ts +5 -0
- package/dist/funding-rates.js +5 -0
- package/dist/funding.d.ts +5 -0
- package/dist/funding.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +458 -0
- package/dist/jobs.d.ts +37 -0
- package/dist/jobs.js +152 -0
- package/dist/liquidity.d.ts +34 -0
- package/dist/liquidity.js +100 -0
- package/dist/mcp-server.d.ts +9 -0
- package/dist/mcp-server.js +1206 -0
- package/dist/pacifica/client.d.ts +111 -0
- package/dist/pacifica/client.js +310 -0
- package/dist/pacifica/constants.d.ts +27 -0
- package/dist/pacifica/constants.js +47 -0
- package/dist/pacifica/deposit.d.ts +14 -0
- package/dist/pacifica/deposit.js +78 -0
- package/dist/pacifica/index.d.ts +6 -0
- package/dist/pacifica/index.js +11 -0
- package/dist/pacifica/signing.d.ts +49 -0
- package/dist/pacifica/signing.js +97 -0
- package/dist/pacifica/types/account.d.ts +42 -0
- package/dist/pacifica/types/account.js +1 -0
- package/dist/pacifica/types/index.d.ts +6 -0
- package/dist/pacifica/types/index.js +6 -0
- package/dist/pacifica/types/lake.d.ts +18 -0
- package/dist/pacifica/types/lake.js +1 -0
- package/dist/pacifica/types/market.d.ts +64 -0
- package/dist/pacifica/types/market.js +1 -0
- package/dist/pacifica/types/order.d.ts +92 -0
- package/dist/pacifica/types/order.js +1 -0
- package/dist/pacifica/types/position.d.ts +25 -0
- package/dist/pacifica/types/position.js +1 -0
- package/dist/pacifica/types/ws.d.ts +34 -0
- package/dist/pacifica/types/ws.js +41 -0
- package/dist/pacifica/ws-client.d.ts +42 -0
- package/dist/pacifica/ws-client.js +180 -0
- package/dist/plan-executor.d.ts +48 -0
- package/dist/plan-executor.js +280 -0
- package/dist/position-history.d.ts +68 -0
- package/dist/position-history.js +222 -0
- package/dist/rebalance.d.ts +64 -0
- package/dist/rebalance.js +142 -0
- package/dist/retry.d.ts +74 -0
- package/dist/retry.js +129 -0
- package/dist/risk.d.ts +48 -0
- package/dist/risk.js +156 -0
- package/dist/settings.d.ts +19 -0
- package/dist/settings.js +45 -0
- package/dist/shared-api.d.ts +5 -0
- package/dist/shared-api.js +5 -0
- package/dist/strategies/dca.d.ts +25 -0
- package/dist/strategies/dca.js +114 -0
- package/dist/strategies/funding-arb.d.ts +15 -0
- package/dist/strategies/funding-arb.js +281 -0
- package/dist/strategies/grid.d.ts +34 -0
- package/dist/strategies/grid.js +185 -0
- package/dist/strategies/trailing-stop.d.ts +17 -0
- package/dist/strategies/trailing-stop.js +121 -0
- package/dist/strategies/twap.d.ts +20 -0
- package/dist/strategies/twap.js +78 -0
- package/dist/trade-validator.d.ts +39 -0
- package/dist/trade-validator.js +154 -0
- package/dist/utils.d.ts +38 -0
- package/dist/utils.js +110 -0
- package/package.json +63 -0
- package/skills/perp-cli/SKILL.md +149 -0
- package/skills/perp-cli/references/commands.md +143 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
|
|
3
|
+
import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
|
|
4
|
+
export function registerDexCommands(program, getAdapter, isJson) {
|
|
5
|
+
const dex = program.command("dex").description("HIP-3 deployed perp dex commands (Hyperliquid)");
|
|
6
|
+
// ── dex list ──
|
|
7
|
+
dex
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("List all available HIP-3 deployed perp dexes")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
await withJsonErrors(isJson(), async () => {
|
|
12
|
+
const adapter = await getAdapter();
|
|
13
|
+
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
14
|
+
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const dexes = await adapter.listDeployedDexes();
|
|
18
|
+
if (isJson())
|
|
19
|
+
return printJson(jsonOk(dexes));
|
|
20
|
+
if (dexes.length === 0) {
|
|
21
|
+
console.log(chalk.gray("\n No deployed dexes found.\n"));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(chalk.cyan.bold("\n HIP-3 Deployed Perp DEXes\n"));
|
|
25
|
+
const rows = dexes.map(d => [
|
|
26
|
+
chalk.white.bold(d.name),
|
|
27
|
+
chalk.gray(d.deployer.slice(0, 10) + "..."),
|
|
28
|
+
String(d.assets.length),
|
|
29
|
+
d.assets.slice(0, 5).join(", ") + (d.assets.length > 5 ? ` +${d.assets.length - 5}` : ""),
|
|
30
|
+
]);
|
|
31
|
+
console.log(makeTable(["DEX", "Deployer", "Assets", "Markets"], rows));
|
|
32
|
+
console.log(chalk.gray(`\n Use --dex <name> to trade on a deployed dex.\n`));
|
|
33
|
+
console.log(chalk.gray(` Example: perp -e hyperliquid --dex xyz market list\n`));
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
// ── dex markets <dex-name> ──
|
|
37
|
+
dex
|
|
38
|
+
.command("markets <dexName>")
|
|
39
|
+
.description("List all markets on a specific HIP-3 dex")
|
|
40
|
+
.action(async (dexName) => {
|
|
41
|
+
await withJsonErrors(isJson(), async () => {
|
|
42
|
+
const adapter = await getAdapter();
|
|
43
|
+
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
44
|
+
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
// Temporarily switch dex, fetch markets, then restore
|
|
48
|
+
const prevDex = adapter.dex;
|
|
49
|
+
adapter.setDex(dexName);
|
|
50
|
+
try {
|
|
51
|
+
const markets = await adapter.getMarkets();
|
|
52
|
+
if (isJson())
|
|
53
|
+
return printJson(jsonOk({ dex: dexName, markets }));
|
|
54
|
+
if (markets.length === 0) {
|
|
55
|
+
console.log(chalk.gray(`\n No markets found on dex "${dexName}".\n`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.cyan.bold(`\n ${dexName.toUpperCase()} DEX Markets\n`));
|
|
59
|
+
const rows = markets.map(m => [
|
|
60
|
+
chalk.white.bold(m.symbol),
|
|
61
|
+
`$${formatUsd(m.markPrice)}`,
|
|
62
|
+
m.fundingRate !== "0" ? `${(Number(m.fundingRate) * 100).toFixed(4)}%` : chalk.gray("-"),
|
|
63
|
+
`$${formatUsd(m.volume24h)}`,
|
|
64
|
+
`$${formatUsd(m.openInterest)}`,
|
|
65
|
+
`${m.maxLeverage}x`,
|
|
66
|
+
]);
|
|
67
|
+
console.log(makeTable(["Symbol", "Mark Price", "Funding", "24h Volume", "OI", "Max Lev"], rows));
|
|
68
|
+
console.log(chalk.gray(`\n Trade: perp -e hyperliquid --dex ${dexName} trade market <symbol> <side> <size>\n`));
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
adapter.setDex(prevDex);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
// ── dex balance <dex-name> ──
|
|
76
|
+
dex
|
|
77
|
+
.command("balance <dexName>")
|
|
78
|
+
.description("Show balance on a specific HIP-3 dex")
|
|
79
|
+
.action(async (dexName) => {
|
|
80
|
+
await withJsonErrors(isJson(), async () => {
|
|
81
|
+
const adapter = await getAdapter();
|
|
82
|
+
if (!(adapter instanceof HyperliquidAdapter)) {
|
|
83
|
+
console.error(chalk.red("\n HIP-3 dexes are only available on Hyperliquid. Use -e hyperliquid.\n"));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const prevDex = adapter.dex;
|
|
87
|
+
adapter.setDex(dexName);
|
|
88
|
+
try {
|
|
89
|
+
const [balance, positions] = await Promise.all([
|
|
90
|
+
adapter.getBalance(),
|
|
91
|
+
adapter.getPositions(),
|
|
92
|
+
]);
|
|
93
|
+
if (isJson())
|
|
94
|
+
return printJson(jsonOk({ dex: dexName, balance, positions }));
|
|
95
|
+
console.log(chalk.cyan.bold(`\n ${dexName.toUpperCase()} DEX Balance\n`));
|
|
96
|
+
console.log(` Equity: $${formatUsd(balance.equity)}`);
|
|
97
|
+
console.log(` Available: $${formatUsd(balance.available)}`);
|
|
98
|
+
console.log(` Margin Used: $${formatUsd(balance.marginUsed)}`);
|
|
99
|
+
console.log(` uPnL: $${formatUsd(balance.unrealizedPnl)}`);
|
|
100
|
+
if (positions.length > 0) {
|
|
101
|
+
console.log(chalk.white.bold("\n Positions:"));
|
|
102
|
+
for (const p of positions) {
|
|
103
|
+
const color = p.side === "long" ? chalk.green : chalk.red;
|
|
104
|
+
console.log(` ${color(p.side.toUpperCase().padEnd(5))} ${chalk.white(p.symbol.padEnd(16))} ${p.size.padEnd(10)} entry: $${Number(p.entryPrice).toFixed(2)} pnl: $${Number(p.unrealizedPnl).toFixed(2)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
console.log();
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
adapter.setDex(prevDex);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { printJson, jsonOk } from "../utils.js";
|
|
3
|
+
import { ENV_FILE, loadEnvFile, setEnvVar, EXCHANGE_ENV_MAP, validateKey } from "./init.js";
|
|
4
|
+
// Resolve exchange alias → env var key
|
|
5
|
+
function resolveEnvKey(nameOrKey) {
|
|
6
|
+
// Direct env var name
|
|
7
|
+
const upper = nameOrKey.toUpperCase();
|
|
8
|
+
for (const info of Object.values(EXCHANGE_ENV_MAP)) {
|
|
9
|
+
if (info.envKey === upper)
|
|
10
|
+
return { envKey: info.envKey, chain: info.chain };
|
|
11
|
+
}
|
|
12
|
+
// Exchange name alias
|
|
13
|
+
const info = EXCHANGE_ENV_MAP[nameOrKey.toLowerCase()];
|
|
14
|
+
if (info)
|
|
15
|
+
return { envKey: info.envKey, chain: info.chain };
|
|
16
|
+
// Common aliases
|
|
17
|
+
const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
|
|
18
|
+
const aliased = aliases[nameOrKey.toLowerCase()];
|
|
19
|
+
if (aliased)
|
|
20
|
+
return resolveEnvKey(aliased);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function registerEnvCommands(program, isJson) {
|
|
24
|
+
const env = program.command("env").description("Manage ~/.perp/.env configuration");
|
|
25
|
+
// ── perp env show ──
|
|
26
|
+
env
|
|
27
|
+
.command("show")
|
|
28
|
+
.description("Show current configuration")
|
|
29
|
+
.action(async () => {
|
|
30
|
+
const stored = loadEnvFile();
|
|
31
|
+
const entries = [];
|
|
32
|
+
for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
|
|
33
|
+
const fromFile = stored[info.envKey];
|
|
34
|
+
const fromEnv = process.env[info.envKey];
|
|
35
|
+
if (fromFile) {
|
|
36
|
+
entries.push({ name: exchange, chain: info.chain, key: fromFile, source: "~/.perp/.env" });
|
|
37
|
+
}
|
|
38
|
+
else if (fromEnv) {
|
|
39
|
+
entries.push({ name: exchange, chain: info.chain, key: fromEnv, source: "environment" });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Derive addresses
|
|
43
|
+
const results = [];
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const { valid, address } = await validateKey(entry.chain, entry.key);
|
|
46
|
+
results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
|
|
47
|
+
}
|
|
48
|
+
if (isJson()) {
|
|
49
|
+
const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
|
|
50
|
+
return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
|
|
51
|
+
}
|
|
52
|
+
console.log(chalk.cyan.bold("\n perp-cli Configuration\n"));
|
|
53
|
+
console.log(` File: ${chalk.gray(ENV_FILE)}\n`);
|
|
54
|
+
if (results.length === 0) {
|
|
55
|
+
console.log(chalk.gray(" No keys configured. Run 'perp init' or 'perp env set <exchange> <key>'\n"));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const { name, address, source } of results) {
|
|
59
|
+
console.log(` ${chalk.cyan(name.padEnd(14))} ${chalk.green(address)} ${chalk.gray(source)}`);
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
});
|
|
63
|
+
// ── perp env set <exchange|key> <value> ──
|
|
64
|
+
env
|
|
65
|
+
.command("set <name> <value>")
|
|
66
|
+
.description("Set a key (exchange name or env var name)")
|
|
67
|
+
.action(async (name, value) => {
|
|
68
|
+
const resolved = resolveEnvKey(name);
|
|
69
|
+
if (resolved) {
|
|
70
|
+
// Validate the key
|
|
71
|
+
const { valid, address } = await validateKey(resolved.chain, value);
|
|
72
|
+
if (!valid) {
|
|
73
|
+
if (isJson()) {
|
|
74
|
+
const { jsonError } = await import("../utils.js");
|
|
75
|
+
return printJson(jsonError("INVALID_PARAMS", `Invalid ${resolved.chain} private key`));
|
|
76
|
+
}
|
|
77
|
+
console.error(chalk.red(`\n Invalid ${resolved.chain} private key.\n`));
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const normalized = resolved.chain === "evm"
|
|
81
|
+
? (value.startsWith("0x") ? value : `0x${value}`)
|
|
82
|
+
: value;
|
|
83
|
+
setEnvVar(resolved.envKey, normalized);
|
|
84
|
+
if (isJson())
|
|
85
|
+
return printJson(jsonOk({ key: resolved.envKey, address, file: ENV_FILE }));
|
|
86
|
+
console.log(chalk.green(`\n ${resolved.envKey} set.`));
|
|
87
|
+
console.log(` Address: ${chalk.gray(address)}`);
|
|
88
|
+
console.log(` File: ${chalk.gray("~/.perp/.env")}\n`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// Raw env var (e.g. LIGHTER_API_KEY, custom vars)
|
|
92
|
+
setEnvVar(name, value);
|
|
93
|
+
if (isJson())
|
|
94
|
+
return printJson(jsonOk({ key: name, file: ENV_FILE }));
|
|
95
|
+
console.log(chalk.green(`\n ${name} set.`));
|
|
96
|
+
console.log(` File: ${chalk.gray("~/.perp/.env")}\n`);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
// ── perp env remove <name> ──
|
|
100
|
+
env
|
|
101
|
+
.command("remove <name>")
|
|
102
|
+
.description("Remove a key from ~/.perp/.env")
|
|
103
|
+
.action(async (name) => {
|
|
104
|
+
const resolved = resolveEnvKey(name);
|
|
105
|
+
const envKey = resolved?.envKey || name;
|
|
106
|
+
const env = loadEnvFile();
|
|
107
|
+
if (!(envKey in env)) {
|
|
108
|
+
if (isJson()) {
|
|
109
|
+
const { jsonError } = await import("../utils.js");
|
|
110
|
+
return printJson(jsonError("NOT_FOUND", `${envKey} not found in ~/.perp/.env`));
|
|
111
|
+
}
|
|
112
|
+
console.log(chalk.gray(`\n ${envKey} not found in ~/.perp/.env\n`));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
delete env[envKey];
|
|
116
|
+
// Rewrite file
|
|
117
|
+
const { writeFileSync } = await import("fs");
|
|
118
|
+
const lines = ["# perp-cli configuration", "# Generated by 'perp init' — edit freely", ""];
|
|
119
|
+
for (const [k, v] of Object.entries(env))
|
|
120
|
+
lines.push(`${k}=${v}`);
|
|
121
|
+
lines.push("");
|
|
122
|
+
writeFileSync(ENV_FILE, lines.join("\n"), { mode: 0o600 });
|
|
123
|
+
if (isJson())
|
|
124
|
+
return printJson(jsonOk({ removed: envKey }));
|
|
125
|
+
console.log(chalk.yellow(`\n ${envKey} removed from ~/.perp/.env\n`));
|
|
126
|
+
});
|
|
127
|
+
// ── perp env path ──
|
|
128
|
+
env
|
|
129
|
+
.command("path")
|
|
130
|
+
.description("Print env file path")
|
|
131
|
+
.action(() => {
|
|
132
|
+
if (isJson())
|
|
133
|
+
return printJson(jsonOk({ path: ENV_FILE }));
|
|
134
|
+
console.log(ENV_FILE);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { makeTable, formatPercent, formatUsd, printJson, jsonOk } from "../utils.js";
|
|
3
|
+
import { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS, } from "../funding-rates.js";
|
|
4
|
+
import { annualizeHourlyRate } from "../funding.js";
|
|
5
|
+
import { saveFundingSnapshot, getHistoricalRates, getCompoundedAnnualReturn, getExchangeCompoundingHours, } from "../funding-history.js";
|
|
6
|
+
export function registerFundingCommands(program, isJson) {
|
|
7
|
+
const funding = program.command("funding").description("Funding rate comparison across exchanges");
|
|
8
|
+
// ── perp funding rates ── (default: show top symbols)
|
|
9
|
+
funding
|
|
10
|
+
.command("rates")
|
|
11
|
+
.description("Show funding rates across all 3 DEXs for top symbols")
|
|
12
|
+
.option("-s, --symbol <symbol>", "Filter to a specific symbol")
|
|
13
|
+
.option("--symbols <list>", "Comma-separated list of symbols")
|
|
14
|
+
.option("--all", "Show all available symbols (not just top ones)")
|
|
15
|
+
.option("--min-spread <pct>", "Minimum annual spread % to show", "0")
|
|
16
|
+
.action(async (opts) => {
|
|
17
|
+
const minSpread = parseFloat(opts.minSpread);
|
|
18
|
+
let filterSymbols;
|
|
19
|
+
if (opts.symbol) {
|
|
20
|
+
filterSymbols = [opts.symbol.toUpperCase()];
|
|
21
|
+
}
|
|
22
|
+
else if (opts.symbols) {
|
|
23
|
+
filterSymbols = opts.symbols.split(",").map(s => s.trim().toUpperCase());
|
|
24
|
+
}
|
|
25
|
+
else if (!opts.all) {
|
|
26
|
+
filterSymbols = TOP_SYMBOLS;
|
|
27
|
+
}
|
|
28
|
+
if (!isJson())
|
|
29
|
+
console.log(chalk.cyan(" Fetching funding rates from all exchanges...\n"));
|
|
30
|
+
const snapshot = await fetchAllFundingRates({
|
|
31
|
+
symbols: filterSymbols,
|
|
32
|
+
minSpread,
|
|
33
|
+
});
|
|
34
|
+
// Persist snapshot for historical tracking (moved from fetchAllFundingRates to avoid side effect)
|
|
35
|
+
try {
|
|
36
|
+
const allRates = snapshot.symbols.flatMap(s => s.rates);
|
|
37
|
+
if (allRates.length > 0)
|
|
38
|
+
saveFundingSnapshot(allRates);
|
|
39
|
+
}
|
|
40
|
+
catch { /* non-critical */ }
|
|
41
|
+
if (isJson())
|
|
42
|
+
return printJson(jsonOk(snapshot));
|
|
43
|
+
printSnapshotTable(snapshot);
|
|
44
|
+
printExchangeStatus(snapshot);
|
|
45
|
+
console.log(chalk.gray(" * Rates shown are current predictions. Actual settled rates may differ.\n"));
|
|
46
|
+
});
|
|
47
|
+
// ── perp funding compare <symbol> ── (detailed single-symbol view)
|
|
48
|
+
funding
|
|
49
|
+
.command("compare <symbol>")
|
|
50
|
+
.description("Detailed funding rate comparison for a single symbol")
|
|
51
|
+
.action(async (symbol) => {
|
|
52
|
+
if (!isJson())
|
|
53
|
+
console.log(chalk.cyan(` Fetching funding rates for ${symbol.toUpperCase()}...\n`));
|
|
54
|
+
const comparison = await fetchSymbolFundingRates(symbol);
|
|
55
|
+
if (!comparison) {
|
|
56
|
+
if (isJson())
|
|
57
|
+
return printJson(jsonOk({ symbol: symbol.toUpperCase(), available: false }));
|
|
58
|
+
console.log(chalk.gray(` ${symbol.toUpperCase()} not found on at least 2 exchanges.\n`));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (isJson())
|
|
62
|
+
return printJson(jsonOk(comparison));
|
|
63
|
+
printDetailedComparison(comparison);
|
|
64
|
+
});
|
|
65
|
+
// ── perp funding spread ── (sorted by spread opportunities)
|
|
66
|
+
funding
|
|
67
|
+
.command("spread")
|
|
68
|
+
.description("Show best funding rate arb opportunities sorted by spread")
|
|
69
|
+
.option("--min <pct>", "Min annual spread to show", "10")
|
|
70
|
+
.option("--top <n>", "Show top N opportunities", "20")
|
|
71
|
+
.action(async (opts) => {
|
|
72
|
+
const minSpread = parseFloat(opts.min);
|
|
73
|
+
const topN = parseInt(opts.top);
|
|
74
|
+
if (!isJson())
|
|
75
|
+
console.log(chalk.cyan(" Scanning funding rate spreads across all exchanges...\n"));
|
|
76
|
+
const snapshot = await fetchAllFundingRates({ minSpread });
|
|
77
|
+
const shown = snapshot.symbols.slice(0, topN);
|
|
78
|
+
if (isJson())
|
|
79
|
+
return printJson(jsonOk(shown));
|
|
80
|
+
if (shown.length === 0) {
|
|
81
|
+
console.log(chalk.gray(` No opportunities above ${minSpread}% annual spread.\n`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log(chalk.cyan.bold(" Funding Rate Arb Opportunities\n"));
|
|
85
|
+
console.log(chalk.gray(" Strategy: Long on low-funding exchange, Short on high-funding exchange\n"));
|
|
86
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
87
|
+
const rows = shown.map(s => {
|
|
88
|
+
const pacRate = s.rates.find(r => r.exchange === "pacifica");
|
|
89
|
+
const hlRate = s.rates.find(r => r.exchange === "hyperliquid");
|
|
90
|
+
const ltRate = s.rates.find(r => r.exchange === "lighter");
|
|
91
|
+
const spreadColor = s.maxSpreadAnnual >= 50 ? chalk.green.bold
|
|
92
|
+
: s.maxSpreadAnnual >= 20 ? chalk.green
|
|
93
|
+
: chalk.yellow;
|
|
94
|
+
return [
|
|
95
|
+
chalk.white.bold(s.symbol),
|
|
96
|
+
`$${formatUsd(s.bestMarkPrice)}`,
|
|
97
|
+
pacRate ? formatPercent(pacRate.fundingRate) : chalk.gray("-"),
|
|
98
|
+
hlRate ? formatPercent(hlRate.fundingRate) : chalk.gray("-"),
|
|
99
|
+
ltRate ? formatPercent(ltRate.fundingRate) : chalk.gray("-"),
|
|
100
|
+
spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`),
|
|
101
|
+
getAvgSpread(s, "avg24h"),
|
|
102
|
+
`${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}`,
|
|
103
|
+
`$${s.estHourlyIncomeUsd.toFixed(4)}/hr`,
|
|
104
|
+
];
|
|
105
|
+
});
|
|
106
|
+
console.log(makeTable(["Symbol", "Price", "Pacifica", "Hyperliquid", "Lighter", "Ann.Spread", "Avg Spread(24h)", "Direction", "Est.Income/$1K"], rows));
|
|
107
|
+
console.log(chalk.gray(`\n ${shown.length} opportunities above ${minSpread}% annual spread`));
|
|
108
|
+
console.log(chalk.gray(` Income estimated for $1,000 notional per leg`));
|
|
109
|
+
console.log(chalk.gray(` Use 'perp arb auto --min-spread ${minSpread}' to auto-trade`));
|
|
110
|
+
console.log(chalk.gray("\n * Rates shown are current predictions. Actual settled rates may differ.\n"));
|
|
111
|
+
});
|
|
112
|
+
// ── perp funding history ── (rate trend over time)
|
|
113
|
+
funding
|
|
114
|
+
.command("history")
|
|
115
|
+
.description("Show funding rate trend over time for a symbol")
|
|
116
|
+
.requiredOption("-s, --symbol <symbol>", "Symbol to show history for")
|
|
117
|
+
.option("--hours <n>", "Number of hours to look back", "24")
|
|
118
|
+
.option("--exchange <ex>", "Filter to a specific exchange")
|
|
119
|
+
.action(async (opts) => {
|
|
120
|
+
const symbol = opts.symbol.toUpperCase();
|
|
121
|
+
const hours = parseInt(opts.hours);
|
|
122
|
+
const exchanges = opts.exchange
|
|
123
|
+
? [opts.exchange.toLowerCase()]
|
|
124
|
+
: ["hyperliquid", "pacifica", "lighter"];
|
|
125
|
+
const endTime = new Date();
|
|
126
|
+
const startTime = new Date(endTime.getTime() - hours * 60 * 60 * 1000);
|
|
127
|
+
if (isJson()) {
|
|
128
|
+
const data = {};
|
|
129
|
+
for (const ex of exchanges) {
|
|
130
|
+
const rates = getHistoricalRates(symbol, ex, startTime, endTime);
|
|
131
|
+
if (rates.length > 0)
|
|
132
|
+
data[ex] = rates;
|
|
133
|
+
}
|
|
134
|
+
return printJson(jsonOk({ symbol, hours, startTime: startTime.toISOString(), endTime: endTime.toISOString(), rates: data }));
|
|
135
|
+
}
|
|
136
|
+
console.log(chalk.cyan.bold(` ${symbol} Funding Rate History (last ${hours}h)\n`));
|
|
137
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
138
|
+
let hasData = false;
|
|
139
|
+
for (const ex of exchanges) {
|
|
140
|
+
const rates = getHistoricalRates(symbol, ex, startTime, endTime);
|
|
141
|
+
if (rates.length === 0)
|
|
142
|
+
continue;
|
|
143
|
+
hasData = true;
|
|
144
|
+
console.log(chalk.white.bold(` ${exAbbr(ex)} (${rates.length} snapshots):`));
|
|
145
|
+
const rows = rates.map(r => {
|
|
146
|
+
const date = new Date(r.ts);
|
|
147
|
+
const timeStr = date.toLocaleString();
|
|
148
|
+
const hourlyPct = (r.hourlyRate * 100).toFixed(6);
|
|
149
|
+
const annualPct = annualizeHourlyRate(r.hourlyRate).toFixed(2);
|
|
150
|
+
const color = r.rate > 0 ? chalk.red : r.rate < 0 ? chalk.green : chalk.white;
|
|
151
|
+
return [
|
|
152
|
+
chalk.gray(timeStr),
|
|
153
|
+
color(formatPercent(r.rate)),
|
|
154
|
+
`${hourlyPct}%/h`,
|
|
155
|
+
`${annualPct}%/yr`,
|
|
156
|
+
];
|
|
157
|
+
});
|
|
158
|
+
console.log(makeTable(["Time", "Raw Rate", "Hourly", "Annualized"], rows));
|
|
159
|
+
console.log();
|
|
160
|
+
}
|
|
161
|
+
if (!hasData) {
|
|
162
|
+
console.log(chalk.gray(` No historical data found for ${symbol} in the last ${hours}h.`));
|
|
163
|
+
console.log(chalk.gray(` Run 'perp funding rates' to start collecting data.\n`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
// ── perp funding monitor ── (live refreshing)
|
|
167
|
+
funding
|
|
168
|
+
.command("monitor")
|
|
169
|
+
.description("Live-monitor funding rates with auto-refresh")
|
|
170
|
+
.option("--min <pct>", "Min annual spread to show", "10")
|
|
171
|
+
.option("--interval <sec>", "Refresh interval in seconds", "30")
|
|
172
|
+
.option("--top <n>", "Show top N", "15")
|
|
173
|
+
.option("--symbols <list>", "Comma-separated symbols to watch")
|
|
174
|
+
.action(async (opts) => {
|
|
175
|
+
const minSpread = parseFloat(opts.min);
|
|
176
|
+
const intervalSec = parseInt(opts.interval);
|
|
177
|
+
const topN = parseInt(opts.top);
|
|
178
|
+
const filterSymbols = opts.symbols?.split(",").map(s => s.trim().toUpperCase());
|
|
179
|
+
let cycle = 0;
|
|
180
|
+
if (!isJson()) {
|
|
181
|
+
console.log(chalk.cyan.bold("\n Funding Rate Monitor"));
|
|
182
|
+
console.log(chalk.gray(` Min spread: ${minSpread}% | Refresh: ${intervalSec}s | Top: ${topN}`));
|
|
183
|
+
if (filterSymbols)
|
|
184
|
+
console.log(chalk.gray(` Symbols: ${filterSymbols.join(", ")}`));
|
|
185
|
+
console.log(chalk.gray(` Press Ctrl+C to stop\n`));
|
|
186
|
+
}
|
|
187
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
188
|
+
while (true) {
|
|
189
|
+
cycle++;
|
|
190
|
+
const ts = new Date().toLocaleTimeString();
|
|
191
|
+
try {
|
|
192
|
+
const snapshot = await fetchAllFundingRates({
|
|
193
|
+
symbols: filterSymbols,
|
|
194
|
+
minSpread,
|
|
195
|
+
});
|
|
196
|
+
const shown = snapshot.symbols.slice(0, topN);
|
|
197
|
+
if (isJson()) {
|
|
198
|
+
printJson(jsonOk({ cycle, timestamp: ts, opportunities: shown }));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Clear previous output
|
|
202
|
+
if (cycle > 1) {
|
|
203
|
+
const linesToClear = shown.length + 4;
|
|
204
|
+
process.stdout.write(`\x1b[${linesToClear}A\x1b[J`);
|
|
205
|
+
}
|
|
206
|
+
console.log(chalk.gray(` ${ts} -- Cycle ${cycle} | ${shown.length} opportunities >= ${minSpread}%\n`));
|
|
207
|
+
if (shown.length === 0) {
|
|
208
|
+
console.log(chalk.gray(` No opportunities found.\n`));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
for (const s of shown) {
|
|
212
|
+
const direction = `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}`;
|
|
213
|
+
const spreadColor = s.maxSpreadAnnual >= 50 ? chalk.green.bold
|
|
214
|
+
: s.maxSpreadAnnual >= 30 ? chalk.green
|
|
215
|
+
: chalk.yellow;
|
|
216
|
+
const rateStrs = [];
|
|
217
|
+
for (const r of s.rates) {
|
|
218
|
+
rateStrs.push(`${exAbbr(r.exchange)}:${(r.fundingRate * 100).toFixed(4)}%`);
|
|
219
|
+
}
|
|
220
|
+
console.log(` ${chalk.white.bold(s.symbol.padEnd(8))} ` +
|
|
221
|
+
`${spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`.padEnd(8))} ` +
|
|
222
|
+
`${direction.padEnd(7)} ` +
|
|
223
|
+
rateStrs.join(" "));
|
|
224
|
+
}
|
|
225
|
+
console.log();
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (!isJson()) {
|
|
231
|
+
console.log(chalk.red(` ${ts} Error: ${err instanceof Error ? err.message : err}\n`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
await new Promise(r => setTimeout(r, intervalSec * 1000));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
// ── Display helpers ──
|
|
239
|
+
function formatAvgRate(rate) {
|
|
240
|
+
if (rate == null)
|
|
241
|
+
return chalk.gray("-");
|
|
242
|
+
const annualPct = annualizeHourlyRate(rate);
|
|
243
|
+
const color = annualPct > 0 ? chalk.red : annualPct < 0 ? chalk.green : chalk.white;
|
|
244
|
+
return color(`${annualPct.toFixed(1)}%`);
|
|
245
|
+
}
|
|
246
|
+
/** Compute an "average spread" across rates that have avg24h data. */
|
|
247
|
+
function getAvgSpread(s, windowKey) {
|
|
248
|
+
const avgs = s.rates
|
|
249
|
+
.filter(r => r.historicalAvg?.[windowKey] != null)
|
|
250
|
+
.map(r => r.historicalAvg[windowKey]);
|
|
251
|
+
if (avgs.length < 2)
|
|
252
|
+
return chalk.gray("-");
|
|
253
|
+
const maxH = Math.max(...avgs);
|
|
254
|
+
const minH = Math.min(...avgs);
|
|
255
|
+
const spreadPct = annualizeHourlyRate(maxH - minH);
|
|
256
|
+
return `${spreadPct.toFixed(1)}%`;
|
|
257
|
+
}
|
|
258
|
+
function printSnapshotTable(snapshot) {
|
|
259
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
260
|
+
if (snapshot.symbols.length === 0) {
|
|
261
|
+
console.log(chalk.gray(" No funding rate data available.\n"));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const rows = snapshot.symbols.map(s => {
|
|
265
|
+
const pacRate = s.rates.find(r => r.exchange === "pacifica");
|
|
266
|
+
const hlRate = s.rates.find(r => r.exchange === "hyperliquid");
|
|
267
|
+
const ltRate = s.rates.find(r => r.exchange === "lighter");
|
|
268
|
+
const spreadColor = s.maxSpreadAnnual >= 30 ? chalk.green
|
|
269
|
+
: s.maxSpreadAnnual >= 10 ? chalk.yellow
|
|
270
|
+
: chalk.white;
|
|
271
|
+
// Use the highest-spread pair's historical averages for the avg columns
|
|
272
|
+
// Show the best exchange's avg for quick reference
|
|
273
|
+
const bestRate = hlRate ?? pacRate ?? ltRate;
|
|
274
|
+
const avg8h = bestRate?.historicalAvg?.avg8h;
|
|
275
|
+
const avg24h = bestRate?.historicalAvg?.avg24h;
|
|
276
|
+
const avg7d = bestRate?.historicalAvg?.avg7d;
|
|
277
|
+
return [
|
|
278
|
+
chalk.white.bold(s.symbol),
|
|
279
|
+
pacRate ? formatPercent(pacRate.fundingRate) : chalk.gray("-"),
|
|
280
|
+
hlRate ? formatPercent(hlRate.fundingRate) : chalk.gray("-"),
|
|
281
|
+
ltRate ? formatPercent(ltRate.fundingRate) : chalk.gray("-"),
|
|
282
|
+
spreadColor(`${s.maxSpreadAnnual.toFixed(1)}%`),
|
|
283
|
+
s.maxSpreadAnnual >= 5 ? `${exAbbr(s.shortExchange)}>${exAbbr(s.longExchange)}` : chalk.gray("-"),
|
|
284
|
+
formatAvgRate(avg8h),
|
|
285
|
+
formatAvgRate(avg24h),
|
|
286
|
+
formatAvgRate(avg7d),
|
|
287
|
+
];
|
|
288
|
+
});
|
|
289
|
+
console.log(makeTable(["Symbol", "Pacifica", "Hyperliquid", "Lighter", "Ann. Spread", "Direction", "Avg 8h", "Avg 24h", "Avg 7d"], rows));
|
|
290
|
+
console.log(chalk.gray(`\n ${snapshot.symbols.length} symbols compared across exchanges.`));
|
|
291
|
+
console.log(chalk.gray(` Rates: All exchanges per 1h. Spread is normalized.`));
|
|
292
|
+
console.log(chalk.gray(` Avg columns show best exchange's historical hourly rate annualized.\n`));
|
|
293
|
+
}
|
|
294
|
+
function printExchangeStatus(snapshot) {
|
|
295
|
+
const statuses = Object.entries(snapshot.exchangeStatus).map(([ex, status]) => {
|
|
296
|
+
const indicator = status === "ok" ? chalk.green("OK") : chalk.red("ERR");
|
|
297
|
+
return `${ex}: ${indicator}`;
|
|
298
|
+
});
|
|
299
|
+
console.log(chalk.gray(` Exchange status: ${statuses.join(" ")}\n`));
|
|
300
|
+
}
|
|
301
|
+
function printDetailedComparison(comparison) {
|
|
302
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : "LT";
|
|
303
|
+
console.log(chalk.cyan.bold(` ${comparison.symbol} Funding Rate Comparison\n`));
|
|
304
|
+
console.log(` Mark Price: $${formatUsd(comparison.bestMarkPrice)}\n`);
|
|
305
|
+
for (const r of comparison.rates) {
|
|
306
|
+
const hourlyPct = (r.hourlyRate * 100).toFixed(6);
|
|
307
|
+
const annualPct = r.annualizedPct.toFixed(2);
|
|
308
|
+
const compHours = getExchangeCompoundingHours(r.exchange);
|
|
309
|
+
const compoundedReturn = getCompoundedAnnualReturn(r.hourlyRate, compHours);
|
|
310
|
+
const compoundedPct = (compoundedReturn * 100).toFixed(2);
|
|
311
|
+
const color = r.fundingRate > 0 ? chalk.red : r.fundingRate < 0 ? chalk.green : chalk.white;
|
|
312
|
+
console.log(` ${chalk.white.bold(exAbbr(r.exchange).padEnd(4))} ` +
|
|
313
|
+
`Raw: ${color(formatPercent(r.fundingRate).padEnd(14))} ` +
|
|
314
|
+
`Hourly: ${hourlyPct}% ` +
|
|
315
|
+
`Annual: ${annualPct}% ` +
|
|
316
|
+
`APY(compounded): ${compoundedPct}%`);
|
|
317
|
+
}
|
|
318
|
+
// Show historical averages if available
|
|
319
|
+
const hasHistorical = comparison.rates.some(r => r.historicalAvg != null);
|
|
320
|
+
if (hasHistorical) {
|
|
321
|
+
console.log(chalk.cyan("\n Historical Averages (annualized):"));
|
|
322
|
+
for (const r of comparison.rates) {
|
|
323
|
+
if (!r.historicalAvg)
|
|
324
|
+
continue;
|
|
325
|
+
const avg8h = r.historicalAvg.avg8h != null ? `${annualizeHourlyRate(r.historicalAvg.avg8h).toFixed(1)}%` : "-";
|
|
326
|
+
const avg24h = r.historicalAvg.avg24h != null ? `${annualizeHourlyRate(r.historicalAvg.avg24h).toFixed(1)}%` : "-";
|
|
327
|
+
const avg7d = r.historicalAvg.avg7d != null ? `${annualizeHourlyRate(r.historicalAvg.avg7d).toFixed(1)}%` : "-";
|
|
328
|
+
console.log(` ${chalk.white.bold(exAbbr(r.exchange).padEnd(4))} ` +
|
|
329
|
+
`8h: ${avg8h.padEnd(10)} 24h: ${avg24h.padEnd(10)} 7d: ${avg7d}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
console.log();
|
|
333
|
+
const spreadColor = comparison.maxSpreadAnnual >= 30 ? chalk.green.bold
|
|
334
|
+
: comparison.maxSpreadAnnual >= 10 ? chalk.yellow
|
|
335
|
+
: chalk.white;
|
|
336
|
+
console.log(` Max Spread: ${spreadColor(`${comparison.maxSpreadAnnual.toFixed(1)}%`)} annual`);
|
|
337
|
+
console.log(` Direction: Long ${exAbbr(comparison.longExchange)} / Short ${exAbbr(comparison.shortExchange)}`);
|
|
338
|
+
console.log(` Est. Income: $${comparison.estHourlyIncomeUsd.toFixed(4)}/hr per $1K notional`);
|
|
339
|
+
// Show income at different position sizes
|
|
340
|
+
const sizes = [1000, 5000, 10000, 50000];
|
|
341
|
+
console.log(chalk.gray("\n Estimated daily income by position size:"));
|
|
342
|
+
for (const size of sizes) {
|
|
343
|
+
const daily = (comparison.estHourlyIncomeUsd / 1000) * size * 24;
|
|
344
|
+
console.log(chalk.gray(` $${formatUsd(size)} notional -> $${daily.toFixed(2)}/day`));
|
|
345
|
+
}
|
|
346
|
+
console.log(chalk.gray("\n * Rates shown are current predictions. Actual settled rates may differ.\n"));
|
|
347
|
+
}
|