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,921 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { makeTable, formatUsd, formatPnl, printJson, jsonOk } from "../../utils.js";
|
|
3
|
+
import { readExecutionLog, logExecution } from "../../execution-log.js";
|
|
4
|
+
import { computeAnnualSpread } from "../../funding.js";
|
|
5
|
+
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates, } from "../../shared-api.js";
|
|
6
|
+
import { computeBasisRisk } from "../../arb-utils.js";
|
|
7
|
+
import { fetchAllBalances, computeRebalancePlan } from "../../rebalance.js";
|
|
8
|
+
import { EXCHANGE_TO_CHAIN, getBestQuote } from "../../bridge-engine.js";
|
|
9
|
+
import { computeEnhancedStats } from "../../arb-history-stats.js";
|
|
10
|
+
// ── Helpers ──
|
|
11
|
+
const EXCHANGES = ["hyperliquid", "lighter", "pacifica"];
|
|
12
|
+
const TAKER_FEE = 0.00035; // ~0.035% typical taker fee
|
|
13
|
+
function formatDuration(ms) {
|
|
14
|
+
const hours = Math.floor(ms / (1000 * 60 * 60));
|
|
15
|
+
const days = Math.floor(hours / 24);
|
|
16
|
+
const remainingHours = hours % 24;
|
|
17
|
+
if (days > 0)
|
|
18
|
+
return `${days}d ${remainingHours}h`;
|
|
19
|
+
if (hours > 0)
|
|
20
|
+
return `${hours}h`;
|
|
21
|
+
const minutes = Math.floor(ms / (1000 * 60));
|
|
22
|
+
return `${minutes}m`;
|
|
23
|
+
}
|
|
24
|
+
async function fetchFundingRatesMap() {
|
|
25
|
+
const rateMap = new Map();
|
|
26
|
+
const [pacAssets, hlAssets, ltDetails, ltFunding] = await Promise.all([
|
|
27
|
+
fetchPacificaPrices(),
|
|
28
|
+
fetchHyperliquidMeta(),
|
|
29
|
+
fetchLighterOrderBookDetails(),
|
|
30
|
+
fetchLighterFundingRates(),
|
|
31
|
+
]);
|
|
32
|
+
const addRate = (sym, exchange, rate, markPrice) => {
|
|
33
|
+
if (!sym)
|
|
34
|
+
return;
|
|
35
|
+
if (!rateMap.has(sym))
|
|
36
|
+
rateMap.set(sym, []);
|
|
37
|
+
rateMap.get(sym).push({ exchange, rate, markPrice });
|
|
38
|
+
};
|
|
39
|
+
for (const p of pacAssets)
|
|
40
|
+
addRate(p.symbol, "pacifica", p.funding, p.mark);
|
|
41
|
+
for (const h of hlAssets)
|
|
42
|
+
addRate(h.symbol, "hyperliquid", h.funding, h.markPx);
|
|
43
|
+
// Lighter: join details + funding by marketId
|
|
44
|
+
const ltPriceMap = new Map(ltDetails.map(d => [d.marketId, d.lastTradePrice]));
|
|
45
|
+
const ltSymMap = new Map(ltDetails.map(d => [d.marketId, d.symbol]));
|
|
46
|
+
for (const fr of ltFunding) {
|
|
47
|
+
const sym = fr.symbol || ltSymMap.get(fr.marketId) || "";
|
|
48
|
+
const mp = fr.markPrice || ltPriceMap.get(fr.marketId) || 0;
|
|
49
|
+
addRate(sym, "lighter", fr.rate, mp);
|
|
50
|
+
}
|
|
51
|
+
return rateMap;
|
|
52
|
+
}
|
|
53
|
+
function findArbEntryForPair(symbol, longExchange, shortExchange) {
|
|
54
|
+
const entries = readExecutionLog({ type: "arb_entry", symbol });
|
|
55
|
+
const successful = entries.filter(e => e.status === "success");
|
|
56
|
+
if (!successful.length)
|
|
57
|
+
return null;
|
|
58
|
+
// Try matching by arbPairId first (new format)
|
|
59
|
+
if (longExchange && shortExchange) {
|
|
60
|
+
const pairId = `${symbol.toUpperCase()}:${longExchange}:${shortExchange}`;
|
|
61
|
+
const byPairId = successful.find(e => e.meta?.arbPairId === pairId);
|
|
62
|
+
if (byPairId)
|
|
63
|
+
return byPairId;
|
|
64
|
+
// Also try matching by exchange field (e.g. "pacifica+hyperliquid")
|
|
65
|
+
const byExchange = successful.find(e => e.meta?.longExchange === longExchange && e.meta?.shortExchange === shortExchange);
|
|
66
|
+
if (byExchange)
|
|
67
|
+
return byExchange;
|
|
68
|
+
}
|
|
69
|
+
// Fallback: return most recent entry for this symbol (legacy records without arbPairId)
|
|
70
|
+
return successful[0];
|
|
71
|
+
}
|
|
72
|
+
function getCurrentSpreadForSymbol(symbol, longExchange, shortExchange, rateMap) {
|
|
73
|
+
const rates = rateMap.get(symbol.toUpperCase());
|
|
74
|
+
if (!rates)
|
|
75
|
+
return null;
|
|
76
|
+
const longRate = rates.find(r => r.exchange === longExchange);
|
|
77
|
+
const shortRate = rates.find(r => r.exchange === shortExchange);
|
|
78
|
+
if (!longRate || !shortRate)
|
|
79
|
+
return null;
|
|
80
|
+
return computeAnnualSpread(shortRate.rate, shortRate.exchange, longRate.rate, longRate.exchange);
|
|
81
|
+
}
|
|
82
|
+
// ── Registration ──
|
|
83
|
+
export function registerArbManageCommands(program, getAdapterForExchange, isJson) {
|
|
84
|
+
const arb = program.commands.find(c => c.name() === "arb");
|
|
85
|
+
if (!arb)
|
|
86
|
+
return;
|
|
87
|
+
// ── arb status ──
|
|
88
|
+
arb
|
|
89
|
+
.command("status")
|
|
90
|
+
.description("Show open arb positions with PnL breakdown")
|
|
91
|
+
.action(async () => {
|
|
92
|
+
if (!isJson())
|
|
93
|
+
console.log(chalk.cyan("\n Checking arb positions across exchanges...\n"));
|
|
94
|
+
// Fetch positions from all exchanges
|
|
95
|
+
const allPositions = [];
|
|
96
|
+
for (const exName of EXCHANGES) {
|
|
97
|
+
try {
|
|
98
|
+
const adapter = await getAdapterForExchange(exName);
|
|
99
|
+
const positions = await adapter.getPositions();
|
|
100
|
+
for (const p of positions) {
|
|
101
|
+
allPositions.push({
|
|
102
|
+
exchange: exName,
|
|
103
|
+
symbol: p.symbol.replace("-PERP", "").toUpperCase(),
|
|
104
|
+
side: p.side,
|
|
105
|
+
size: Math.abs(Number(p.size)),
|
|
106
|
+
entryPrice: Number(p.entryPrice),
|
|
107
|
+
markPrice: Number(p.markPrice),
|
|
108
|
+
unrealizedPnl: Number(p.unrealizedPnl),
|
|
109
|
+
leverage: p.leverage,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// exchange not configured, skip
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Group by symbol to detect arb pairs
|
|
118
|
+
const bySymbol = new Map();
|
|
119
|
+
for (const p of allPositions) {
|
|
120
|
+
if (!bySymbol.has(p.symbol))
|
|
121
|
+
bySymbol.set(p.symbol, []);
|
|
122
|
+
bySymbol.get(p.symbol).push(p);
|
|
123
|
+
}
|
|
124
|
+
// Find arb pairs: same symbol, different exchanges, one long + one short
|
|
125
|
+
const arbPairs = [];
|
|
126
|
+
// Fetch current funding rates for spread calculation
|
|
127
|
+
let rateMap;
|
|
128
|
+
try {
|
|
129
|
+
rateMap = await fetchFundingRatesMap();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
rateMap = new Map();
|
|
133
|
+
}
|
|
134
|
+
for (const [symbol, positions] of bySymbol) {
|
|
135
|
+
const longs = positions.filter(p => p.side === "long");
|
|
136
|
+
const shorts = positions.filter(p => p.side === "short");
|
|
137
|
+
// Match longs and shorts on different exchanges
|
|
138
|
+
for (const longPos of longs) {
|
|
139
|
+
for (const shortPos of shorts) {
|
|
140
|
+
if (longPos.exchange === shortPos.exchange)
|
|
141
|
+
continue;
|
|
142
|
+
// Look up entry info from execution log (match by pair)
|
|
143
|
+
const entryLog = findArbEntryForPair(symbol, longPos.exchange, shortPos.exchange);
|
|
144
|
+
const entrySpread = entryLog?.meta?.spread ?? null;
|
|
145
|
+
const entryTime = entryLog?.timestamp ?? null;
|
|
146
|
+
const holdDurationMs = entryTime ? Date.now() - new Date(entryTime).getTime() : null;
|
|
147
|
+
const holdDuration = holdDurationMs ? formatDuration(holdDurationMs) : null;
|
|
148
|
+
// Current spread
|
|
149
|
+
const currentSpread = getCurrentSpreadForSymbol(symbol, longPos.exchange, shortPos.exchange, rateMap);
|
|
150
|
+
// Position notional values
|
|
151
|
+
const longNotional = longPos.size * longPos.markPrice;
|
|
152
|
+
const shortNotional = shortPos.size * shortPos.markPrice;
|
|
153
|
+
const avgNotional = (longNotional + shortNotional) / 2;
|
|
154
|
+
// Estimated funding income (from hold time and current spread)
|
|
155
|
+
let estimatedFundingIncome = 0;
|
|
156
|
+
if (holdDurationMs && currentSpread) {
|
|
157
|
+
const holdHours = holdDurationMs / (1000 * 60 * 60);
|
|
158
|
+
// Annual spread % → hourly income on notional
|
|
159
|
+
estimatedFundingIncome = (currentSpread / 100) / (24 * 365) * avgNotional * holdHours;
|
|
160
|
+
}
|
|
161
|
+
// Estimated fees (entry + exit)
|
|
162
|
+
const entryFees = (longPos.size * longPos.entryPrice + shortPos.size * shortPos.entryPrice) * TAKER_FEE;
|
|
163
|
+
const exitFees = (longNotional + shortNotional) * TAKER_FEE;
|
|
164
|
+
const totalFees = entryFees + exitFees;
|
|
165
|
+
// Net PnL
|
|
166
|
+
const totalUpnl = longPos.unrealizedPnl + shortPos.unrealizedPnl;
|
|
167
|
+
const netPnl = totalUpnl + estimatedFundingIncome - totalFees;
|
|
168
|
+
arbPairs.push({
|
|
169
|
+
symbol,
|
|
170
|
+
longExchange: longPos.exchange,
|
|
171
|
+
shortExchange: shortPos.exchange,
|
|
172
|
+
longPosition: {
|
|
173
|
+
side: "long",
|
|
174
|
+
size: longPos.size,
|
|
175
|
+
entryPrice: longPos.entryPrice,
|
|
176
|
+
markPrice: longPos.markPrice,
|
|
177
|
+
unrealizedPnl: longPos.unrealizedPnl,
|
|
178
|
+
leverage: longPos.leverage,
|
|
179
|
+
notionalUsd: longNotional,
|
|
180
|
+
},
|
|
181
|
+
shortPosition: {
|
|
182
|
+
side: "short",
|
|
183
|
+
size: shortPos.size,
|
|
184
|
+
entryPrice: shortPos.entryPrice,
|
|
185
|
+
markPrice: shortPos.markPrice,
|
|
186
|
+
unrealizedPnl: shortPos.unrealizedPnl,
|
|
187
|
+
leverage: shortPos.leverage,
|
|
188
|
+
notionalUsd: shortNotional,
|
|
189
|
+
},
|
|
190
|
+
entrySpread: entrySpread !== null ? Number(entrySpread) : null,
|
|
191
|
+
currentSpread,
|
|
192
|
+
holdDuration,
|
|
193
|
+
holdDurationMs,
|
|
194
|
+
estimatedFundingIncome,
|
|
195
|
+
estimatedFees: totalFees,
|
|
196
|
+
unrealizedPnl: totalUpnl,
|
|
197
|
+
netPnl,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (isJson()) {
|
|
203
|
+
return printJson(jsonOk(arbPairs));
|
|
204
|
+
}
|
|
205
|
+
if (arbPairs.length === 0) {
|
|
206
|
+
console.log(chalk.gray(" No open arb positions found.\n"));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
console.log(chalk.cyan.bold(" Open Arb Positions\n"));
|
|
210
|
+
const rows = arbPairs.map(p => {
|
|
211
|
+
const spreadStr = p.currentSpread !== null ? `${p.currentSpread.toFixed(1)}%` : "-";
|
|
212
|
+
const entrySpreadStr = p.entrySpread !== null ? `${p.entrySpread.toFixed(1)}%` : "-";
|
|
213
|
+
const holdStr = p.holdDuration ?? "-";
|
|
214
|
+
const avgNotional = (p.longPosition.notionalUsd + p.shortPosition.notionalUsd) / 2;
|
|
215
|
+
// Compute basis risk from mark prices
|
|
216
|
+
const basis = computeBasisRisk(p.longPosition.markPrice, p.shortPosition.markPrice);
|
|
217
|
+
const basisStr = basis.divergencePct > 0
|
|
218
|
+
? (basis.warning
|
|
219
|
+
? chalk.red(`${basis.divergencePct.toFixed(1)}%`)
|
|
220
|
+
: chalk.gray(`${basis.divergencePct.toFixed(1)}%`))
|
|
221
|
+
: chalk.gray("-");
|
|
222
|
+
return [
|
|
223
|
+
chalk.white.bold(p.symbol),
|
|
224
|
+
chalk.green(p.longExchange),
|
|
225
|
+
chalk.red(p.shortExchange),
|
|
226
|
+
`$${formatUsd(avgNotional)}`,
|
|
227
|
+
`$${p.longPosition.entryPrice.toFixed(2)} / $${p.shortPosition.entryPrice.toFixed(2)}`,
|
|
228
|
+
`$${p.longPosition.markPrice.toFixed(2)}`,
|
|
229
|
+
formatPnl(p.unrealizedPnl),
|
|
230
|
+
chalk.yellow(`$${p.estimatedFundingIncome.toFixed(4)}`),
|
|
231
|
+
`${entrySpreadStr} -> ${spreadStr}`,
|
|
232
|
+
basisStr,
|
|
233
|
+
holdStr,
|
|
234
|
+
formatPnl(p.netPnl),
|
|
235
|
+
];
|
|
236
|
+
});
|
|
237
|
+
console.log(makeTable(["Symbol", "Long", "Short", "Size", "Entry", "Mark", "uPnL", "Funding", "Spread", "Basis", "Hold", "Net PnL"], rows));
|
|
238
|
+
// Summary
|
|
239
|
+
const totalUpnl = arbPairs.reduce((s, p) => s + p.unrealizedPnl, 0);
|
|
240
|
+
const totalFunding = arbPairs.reduce((s, p) => s + p.estimatedFundingIncome, 0);
|
|
241
|
+
const totalFees = arbPairs.reduce((s, p) => s + p.estimatedFees, 0);
|
|
242
|
+
const totalNet = arbPairs.reduce((s, p) => s + p.netPnl, 0);
|
|
243
|
+
console.log(chalk.white.bold("\n Summary"));
|
|
244
|
+
console.log(` Positions: ${arbPairs.length}`);
|
|
245
|
+
console.log(` Unrealized PnL: ${formatPnl(totalUpnl)}`);
|
|
246
|
+
console.log(` Est. Funding: ${chalk.yellow(`$${totalFunding.toFixed(4)}`)}`);
|
|
247
|
+
console.log(` Est. Fees: ${chalk.red(`-$${totalFees.toFixed(4)}`)}`);
|
|
248
|
+
console.log(` Net PnL: ${formatPnl(totalNet)}`);
|
|
249
|
+
console.log(chalk.gray(` (Fees assume ${(TAKER_FEE * 100).toFixed(3)}% taker for entry + exit.)\n`));
|
|
250
|
+
});
|
|
251
|
+
// ── arb close ──
|
|
252
|
+
arb
|
|
253
|
+
.command("close <symbol>")
|
|
254
|
+
.description("Manually close an arb position on both exchanges")
|
|
255
|
+
.option("--dry-run", "Show what would happen without executing")
|
|
256
|
+
.option("--pair <pair>", "Specify arb pair as longExchange:shortExchange (e.g. pacifica:hyperliquid)")
|
|
257
|
+
.action(async (symbol, opts) => {
|
|
258
|
+
const sym = symbol.toUpperCase();
|
|
259
|
+
const dryRun = !!opts.dryRun || process.argv.includes("--dry-run");
|
|
260
|
+
if (!isJson()) {
|
|
261
|
+
console.log(chalk.cyan(`\n Closing arb position for ${sym}...\n`));
|
|
262
|
+
if (dryRun)
|
|
263
|
+
console.log(chalk.yellow(" Mode: DRY RUN (no trades will be executed)\n"));
|
|
264
|
+
}
|
|
265
|
+
// Find positions for this symbol across all exchanges
|
|
266
|
+
const allPositions = [];
|
|
267
|
+
for (const exName of EXCHANGES) {
|
|
268
|
+
try {
|
|
269
|
+
const adapter = await getAdapterForExchange(exName);
|
|
270
|
+
const positions = await adapter.getPositions();
|
|
271
|
+
for (const p of positions) {
|
|
272
|
+
const normalized = p.symbol.replace("-PERP", "").toUpperCase();
|
|
273
|
+
if (normalized === sym) {
|
|
274
|
+
allPositions.push({
|
|
275
|
+
exchange: exName,
|
|
276
|
+
symbol: normalized,
|
|
277
|
+
rawSymbol: p.symbol,
|
|
278
|
+
side: p.side,
|
|
279
|
+
size: Math.abs(Number(p.size)),
|
|
280
|
+
entryPrice: Number(p.entryPrice),
|
|
281
|
+
markPrice: Number(p.markPrice),
|
|
282
|
+
unrealizedPnl: Number(p.unrealizedPnl),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
// exchange not configured, skip
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// Find all possible arb pairs (long on one exchange, short on another)
|
|
292
|
+
const possiblePairs = [];
|
|
293
|
+
const longs = allPositions.filter(p => p.side === "long");
|
|
294
|
+
const shorts = allPositions.filter(p => p.side === "short");
|
|
295
|
+
for (const l of longs) {
|
|
296
|
+
for (const s of shorts) {
|
|
297
|
+
if (l.exchange !== s.exchange)
|
|
298
|
+
possiblePairs.push({ long: l, short: s });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// If --pair specified, filter to that specific pair
|
|
302
|
+
let longPos;
|
|
303
|
+
let shortPos;
|
|
304
|
+
if (opts.pair) {
|
|
305
|
+
const [longEx, shortEx] = opts.pair.split(":");
|
|
306
|
+
const match = possiblePairs.find(p => p.long.exchange === longEx && p.short.exchange === shortEx);
|
|
307
|
+
if (match) {
|
|
308
|
+
longPos = match.long;
|
|
309
|
+
shortPos = match.short;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
else if (possiblePairs.length === 1) {
|
|
313
|
+
longPos = possiblePairs[0].long;
|
|
314
|
+
shortPos = possiblePairs[0].short;
|
|
315
|
+
}
|
|
316
|
+
else if (possiblePairs.length > 1) {
|
|
317
|
+
// Multiple pairs found — require explicit --pair selection
|
|
318
|
+
const msg = `Multiple arb pairs found for ${sym}. Use --pair to specify which one to close.`;
|
|
319
|
+
if (isJson()) {
|
|
320
|
+
return printJson(jsonOk({
|
|
321
|
+
error: msg,
|
|
322
|
+
pairs: possiblePairs.map(p => ({
|
|
323
|
+
arbPairId: `${sym}:${p.long.exchange}:${p.short.exchange}`,
|
|
324
|
+
longExchange: p.long.exchange,
|
|
325
|
+
shortExchange: p.short.exchange,
|
|
326
|
+
longSize: p.long.size,
|
|
327
|
+
shortSize: p.short.size,
|
|
328
|
+
})),
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
console.log(chalk.red(` ${msg}\n`));
|
|
332
|
+
console.log(chalk.white(" Available pairs:"));
|
|
333
|
+
for (const p of possiblePairs) {
|
|
334
|
+
console.log(chalk.gray(` --pair ${p.long.exchange}:${p.short.exchange} (long ${p.long.size} / short ${p.short.size})`));
|
|
335
|
+
}
|
|
336
|
+
console.log();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (!longPos || !shortPos) {
|
|
340
|
+
const msg = `No arb pair found for ${sym}. Need long and short on different exchanges.`;
|
|
341
|
+
if (isJson())
|
|
342
|
+
return printJson(jsonOk({ error: msg, positions: allPositions }));
|
|
343
|
+
console.log(chalk.red(` ${msg}`));
|
|
344
|
+
if (allPositions.length > 0) {
|
|
345
|
+
console.log(chalk.gray(` Found ${allPositions.length} position(s) but no matching arb pair:`));
|
|
346
|
+
for (const p of allPositions) {
|
|
347
|
+
console.log(chalk.gray(` ${p.side.toUpperCase()} ${p.exchange} size=${p.size}`));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
console.log();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
// Calculate estimated PnL
|
|
354
|
+
const totalUpnl = longPos.unrealizedPnl + shortPos.unrealizedPnl;
|
|
355
|
+
const entryFees = (longPos.size * longPos.entryPrice + shortPos.size * shortPos.entryPrice) * TAKER_FEE;
|
|
356
|
+
const exitFees = (longPos.size * longPos.markPrice + shortPos.size * shortPos.markPrice) * TAKER_FEE;
|
|
357
|
+
const totalFees = entryFees + exitFees;
|
|
358
|
+
const netPnl = totalUpnl - totalFees;
|
|
359
|
+
if (!isJson()) {
|
|
360
|
+
console.log(chalk.white.bold(` ${sym} Arb Position`));
|
|
361
|
+
console.log(` Long: ${longPos.exchange} | size: ${longPos.size} | entry: $${longPos.entryPrice.toFixed(4)} | mark: $${longPos.markPrice.toFixed(4)} | uPnL: ${formatPnl(longPos.unrealizedPnl)}`);
|
|
362
|
+
console.log(` Short: ${shortPos.exchange} | size: ${shortPos.size} | entry: $${shortPos.entryPrice.toFixed(4)} | mark: $${shortPos.markPrice.toFixed(4)} | uPnL: ${formatPnl(shortPos.unrealizedPnl)}`);
|
|
363
|
+
console.log();
|
|
364
|
+
console.log(` Total uPnL: ${formatPnl(totalUpnl)}`);
|
|
365
|
+
console.log(` Est. Fees: ${chalk.red(`-$${totalFees.toFixed(4)}`)}`);
|
|
366
|
+
console.log(` Net PnL: ${formatPnl(netPnl)}`);
|
|
367
|
+
console.log();
|
|
368
|
+
}
|
|
369
|
+
if (dryRun) {
|
|
370
|
+
const result = {
|
|
371
|
+
dryRun: true,
|
|
372
|
+
symbol: sym,
|
|
373
|
+
longExchange: longPos.exchange,
|
|
374
|
+
shortExchange: shortPos.exchange,
|
|
375
|
+
longSize: longPos.size,
|
|
376
|
+
shortSize: shortPos.size,
|
|
377
|
+
unrealizedPnl: totalUpnl,
|
|
378
|
+
estimatedFees: totalFees,
|
|
379
|
+
netPnl,
|
|
380
|
+
actions: [
|
|
381
|
+
{ exchange: longPos.exchange, action: "sell", symbol: longPos.rawSymbol, size: String(longPos.size) },
|
|
382
|
+
{ exchange: shortPos.exchange, action: "buy", symbol: shortPos.rawSymbol, size: String(shortPos.size) },
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
if (isJson())
|
|
386
|
+
return printJson(jsonOk(result));
|
|
387
|
+
console.log(chalk.yellow(" Would execute:"));
|
|
388
|
+
console.log(chalk.yellow(` SELL ${longPos.size} ${longPos.rawSymbol} on ${longPos.exchange} (close long)`));
|
|
389
|
+
console.log(chalk.yellow(` BUY ${shortPos.size} ${shortPos.rawSymbol} on ${shortPos.exchange} (close short)\n`));
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
// Execute close on both exchanges
|
|
393
|
+
const results = [];
|
|
394
|
+
// Close both legs concurrently
|
|
395
|
+
const closePromises = [
|
|
396
|
+
(async () => {
|
|
397
|
+
try {
|
|
398
|
+
const adapter = await getAdapterForExchange(longPos.exchange);
|
|
399
|
+
await adapter.marketOrder(longPos.rawSymbol, "sell", String(longPos.size));
|
|
400
|
+
results.push({ exchange: longPos.exchange, action: "sell (close long)", status: "success" });
|
|
401
|
+
if (!isJson())
|
|
402
|
+
console.log(chalk.green(` Closed long on ${longPos.exchange}: SELL ${longPos.size} ${sym}`));
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
406
|
+
results.push({ exchange: longPos.exchange, action: "sell (close long)", status: "failed", error: msg });
|
|
407
|
+
if (!isJson())
|
|
408
|
+
console.error(chalk.red(` Failed to close long on ${longPos.exchange}: ${msg}`));
|
|
409
|
+
}
|
|
410
|
+
})(),
|
|
411
|
+
(async () => {
|
|
412
|
+
try {
|
|
413
|
+
const adapter = await getAdapterForExchange(shortPos.exchange);
|
|
414
|
+
await adapter.marketOrder(shortPos.rawSymbol, "buy", String(shortPos.size));
|
|
415
|
+
results.push({ exchange: shortPos.exchange, action: "buy (close short)", status: "success" });
|
|
416
|
+
if (!isJson())
|
|
417
|
+
console.log(chalk.green(` Closed short on ${shortPos.exchange}: BUY ${shortPos.size} ${sym}`));
|
|
418
|
+
}
|
|
419
|
+
catch (err) {
|
|
420
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
421
|
+
results.push({ exchange: shortPos.exchange, action: "buy (close short)", status: "failed", error: msg });
|
|
422
|
+
if (!isJson())
|
|
423
|
+
console.error(chalk.red(` Failed to close short on ${shortPos.exchange}: ${msg}`));
|
|
424
|
+
}
|
|
425
|
+
})(),
|
|
426
|
+
];
|
|
427
|
+
await Promise.all(closePromises);
|
|
428
|
+
const allSuccess = results.every(r => r.status === "success");
|
|
429
|
+
const anySuccess = results.some(r => r.status === "success");
|
|
430
|
+
// Log to execution log
|
|
431
|
+
const arbPairId = `${sym}:${longPos.exchange}:${shortPos.exchange}`;
|
|
432
|
+
logExecution({
|
|
433
|
+
type: "arb_close",
|
|
434
|
+
exchange: `${longPos.exchange}+${shortPos.exchange}`,
|
|
435
|
+
symbol: sym,
|
|
436
|
+
side: "close",
|
|
437
|
+
size: String(Math.max(longPos.size, shortPos.size)),
|
|
438
|
+
status: allSuccess ? "success" : "failed",
|
|
439
|
+
dryRun: false,
|
|
440
|
+
error: allSuccess ? undefined : results.filter(r => r.error).map(r => `${r.exchange}: ${r.error}`).join("; "),
|
|
441
|
+
meta: {
|
|
442
|
+
arbPairId,
|
|
443
|
+
longExchange: longPos.exchange,
|
|
444
|
+
shortExchange: shortPos.exchange,
|
|
445
|
+
unrealizedPnl: totalUpnl,
|
|
446
|
+
estimatedFees: totalFees,
|
|
447
|
+
netPnl,
|
|
448
|
+
results,
|
|
449
|
+
exitReason: "manual",
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
if (isJson()) {
|
|
453
|
+
return printJson(jsonOk({
|
|
454
|
+
symbol: sym,
|
|
455
|
+
longExchange: longPos.exchange,
|
|
456
|
+
shortExchange: shortPos.exchange,
|
|
457
|
+
status: allSuccess ? "success" : anySuccess ? "partial" : "failed",
|
|
458
|
+
unrealizedPnl: totalUpnl,
|
|
459
|
+
estimatedFees: totalFees,
|
|
460
|
+
netPnl,
|
|
461
|
+
results,
|
|
462
|
+
}));
|
|
463
|
+
}
|
|
464
|
+
if (!allSuccess && anySuccess) {
|
|
465
|
+
console.log(chalk.yellow(`\n Warning: Partial close — one leg failed. Manual intervention may be needed.\n`));
|
|
466
|
+
}
|
|
467
|
+
else if (allSuccess) {
|
|
468
|
+
console.log(chalk.green(`\n Arb position ${sym} closed successfully.\n`));
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
console.log(chalk.red(`\n Failed to close arb position ${sym}. Both legs failed.\n`));
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
// ── arb history ──
|
|
475
|
+
arb
|
|
476
|
+
.command("history")
|
|
477
|
+
.description("Past arb trade performance and statistics")
|
|
478
|
+
.option("--period <days>", "Number of days to look back", "30")
|
|
479
|
+
.action(async (opts) => {
|
|
480
|
+
const periodDays = parseInt(opts.period);
|
|
481
|
+
const sinceDate = new Date(Date.now() - periodDays * 24 * 60 * 60 * 1000).toISOString();
|
|
482
|
+
if (!isJson())
|
|
483
|
+
console.log(chalk.cyan(`\n Arb trade history (last ${periodDays} days)\n`));
|
|
484
|
+
// Read arb-related execution log entries
|
|
485
|
+
const arbEntries = readExecutionLog({ since: sinceDate })
|
|
486
|
+
.filter(r => r.type === "arb_entry" || r.type === "arb_close");
|
|
487
|
+
if (arbEntries.length === 0) {
|
|
488
|
+
const result = {
|
|
489
|
+
trades: [],
|
|
490
|
+
summary: {
|
|
491
|
+
totalTrades: 0,
|
|
492
|
+
completedTrades: 0,
|
|
493
|
+
winRate: 0,
|
|
494
|
+
avgHoldTime: null,
|
|
495
|
+
totalNetPnl: 0,
|
|
496
|
+
bestTrade: null,
|
|
497
|
+
worstTrade: null,
|
|
498
|
+
avgEntrySpread: 0,
|
|
499
|
+
avgExitSpread: 0,
|
|
500
|
+
avgSpreadDecay: 0,
|
|
501
|
+
byExchangePair: [],
|
|
502
|
+
byTimeOfDay: [],
|
|
503
|
+
optimalHoldTime: null,
|
|
504
|
+
},
|
|
505
|
+
period: periodDays,
|
|
506
|
+
};
|
|
507
|
+
if (isJson())
|
|
508
|
+
return printJson(jsonOk(result));
|
|
509
|
+
console.log(chalk.gray(" No arb trades found in this period.\n"));
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
// Group entries by arbPairId (or symbol fallback for legacy) to match entry/exit pairs
|
|
513
|
+
const entryMap = new Map();
|
|
514
|
+
const closeMap = new Map();
|
|
515
|
+
function getPairKey(record) {
|
|
516
|
+
// Use arbPairId if available (new format), fallback to symbol (legacy)
|
|
517
|
+
const arbPairId = record.meta?.arbPairId;
|
|
518
|
+
if (arbPairId)
|
|
519
|
+
return arbPairId;
|
|
520
|
+
return record.symbol.toUpperCase();
|
|
521
|
+
}
|
|
522
|
+
for (const entry of arbEntries) {
|
|
523
|
+
const key = getPairKey(entry);
|
|
524
|
+
if (entry.type === "arb_entry") {
|
|
525
|
+
if (!entryMap.has(key))
|
|
526
|
+
entryMap.set(key, []);
|
|
527
|
+
entryMap.get(key).push(entry);
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
if (!closeMap.has(key))
|
|
531
|
+
closeMap.set(key, []);
|
|
532
|
+
closeMap.get(key).push(entry);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Build trade history by pairing entries with closes
|
|
536
|
+
const trades = [];
|
|
537
|
+
for (const [symbol, entries] of entryMap) {
|
|
538
|
+
const closes = closeMap.get(symbol) ?? [];
|
|
539
|
+
// Sort entries oldest first for matching
|
|
540
|
+
entries.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
541
|
+
closes.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
542
|
+
for (let i = 0; i < entries.length; i++) {
|
|
543
|
+
const entry = entries[i];
|
|
544
|
+
// Find matching close (first close after this entry)
|
|
545
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
546
|
+
const matchingClose = closes.find(c => new Date(c.timestamp).getTime() > entryTime);
|
|
547
|
+
const exchanges = entry.exchange;
|
|
548
|
+
const entrySpread = entry.meta?.spread ?? null;
|
|
549
|
+
if (matchingClose) {
|
|
550
|
+
// Remove matched close to avoid double-matching
|
|
551
|
+
const closeIdx = closes.indexOf(matchingClose);
|
|
552
|
+
closes.splice(closeIdx, 1);
|
|
553
|
+
const closeTime = new Date(matchingClose.timestamp).getTime();
|
|
554
|
+
const holdMs = closeTime - entryTime;
|
|
555
|
+
const holdHours = holdMs / (1000 * 60 * 60);
|
|
556
|
+
// Estimate PnL from metadata
|
|
557
|
+
const exitSpread = matchingClose.meta?.currentSpread ?? null;
|
|
558
|
+
const netPnl = matchingClose.meta?.netPnl ?? null;
|
|
559
|
+
const upnl = matchingClose.meta?.unrealizedPnl ?? null;
|
|
560
|
+
const exitReason = matchingClose.meta?.exitReason ?? null;
|
|
561
|
+
// Estimate funding income based on hold time and entry spread
|
|
562
|
+
const avgSpread = entrySpread ? entrySpread : 0;
|
|
563
|
+
const sizeUsd = entry.meta?.markPrice
|
|
564
|
+
? Number(entry.size) * Number(entry.meta.markPrice)
|
|
565
|
+
: 0;
|
|
566
|
+
const estimatedFunding = sizeUsd > 0
|
|
567
|
+
? (avgSpread / 100) / (24 * 365) * sizeUsd * holdHours
|
|
568
|
+
: 0;
|
|
569
|
+
// Fees estimate
|
|
570
|
+
const fees = sizeUsd * TAKER_FEE * 2 * 2; // entry + exit, both legs
|
|
571
|
+
trades.push({
|
|
572
|
+
symbol,
|
|
573
|
+
exchanges,
|
|
574
|
+
entryDate: entry.timestamp,
|
|
575
|
+
exitDate: matchingClose.timestamp,
|
|
576
|
+
holdDuration: formatDuration(holdMs),
|
|
577
|
+
holdDurationMs: holdMs,
|
|
578
|
+
entrySpread,
|
|
579
|
+
exitSpread,
|
|
580
|
+
size: entry.size,
|
|
581
|
+
grossReturn: upnl !== null ? Number(upnl) : 0,
|
|
582
|
+
fees,
|
|
583
|
+
fundingIncome: estimatedFunding,
|
|
584
|
+
netReturn: netPnl !== null ? Number(netPnl) : (upnl !== null ? Number(upnl) + estimatedFunding - fees : 0),
|
|
585
|
+
status: matchingClose.status === "success" ? "completed" : "failed",
|
|
586
|
+
exitReason,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
else {
|
|
590
|
+
// Open trade (no matching close)
|
|
591
|
+
const holdMs = Date.now() - entryTime;
|
|
592
|
+
trades.push({
|
|
593
|
+
symbol,
|
|
594
|
+
exchanges,
|
|
595
|
+
entryDate: entry.timestamp,
|
|
596
|
+
exitDate: null,
|
|
597
|
+
holdDuration: formatDuration(holdMs),
|
|
598
|
+
holdDurationMs: holdMs,
|
|
599
|
+
entrySpread,
|
|
600
|
+
exitSpread: null,
|
|
601
|
+
size: entry.size,
|
|
602
|
+
grossReturn: 0,
|
|
603
|
+
fees: 0,
|
|
604
|
+
fundingIncome: 0,
|
|
605
|
+
netReturn: 0,
|
|
606
|
+
status: "open",
|
|
607
|
+
exitReason: null,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Sort by entry date, newest first
|
|
613
|
+
trades.sort((a, b) => new Date(b.entryDate).getTime() - new Date(a.entryDate).getTime());
|
|
614
|
+
// Compute summary statistics
|
|
615
|
+
const completedTrades = trades.filter(t => t.status === "completed");
|
|
616
|
+
const winners = completedTrades.filter(t => t.netReturn > 0);
|
|
617
|
+
const totalNetPnl = completedTrades.reduce((s, t) => s + t.netReturn, 0);
|
|
618
|
+
const totalFunding = completedTrades.reduce((s, t) => s + t.fundingIncome, 0);
|
|
619
|
+
const totalFees = completedTrades.reduce((s, t) => s + t.fees, 0);
|
|
620
|
+
const avgHoldMs = completedTrades.length > 0
|
|
621
|
+
? completedTrades.reduce((s, t) => s + t.holdDurationMs, 0) / completedTrades.length
|
|
622
|
+
: 0;
|
|
623
|
+
const bestTrade = completedTrades.length > 0
|
|
624
|
+
? completedTrades.reduce((best, t) => t.netReturn > best.netReturn ? t : best)
|
|
625
|
+
: null;
|
|
626
|
+
const worstTrade = completedTrades.length > 0
|
|
627
|
+
? completedTrades.reduce((worst, t) => t.netReturn < worst.netReturn ? t : worst)
|
|
628
|
+
: null;
|
|
629
|
+
// Compute enhanced analytics
|
|
630
|
+
const statsInput = trades.map(t => ({
|
|
631
|
+
symbol: t.symbol,
|
|
632
|
+
exchanges: t.exchanges,
|
|
633
|
+
entryDate: t.entryDate,
|
|
634
|
+
exitDate: t.exitDate,
|
|
635
|
+
holdDurationMs: t.holdDurationMs,
|
|
636
|
+
entrySpread: t.entrySpread,
|
|
637
|
+
exitSpread: t.exitSpread,
|
|
638
|
+
netReturn: t.netReturn,
|
|
639
|
+
status: t.status,
|
|
640
|
+
}));
|
|
641
|
+
const enhanced = computeEnhancedStats(statsInput);
|
|
642
|
+
const summary = {
|
|
643
|
+
totalTrades: trades.length,
|
|
644
|
+
completedTrades: completedTrades.length,
|
|
645
|
+
openTrades: trades.filter(t => t.status === "open").length,
|
|
646
|
+
failedTrades: trades.filter(t => t.status === "failed").length,
|
|
647
|
+
winRate: completedTrades.length > 0 ? (winners.length / completedTrades.length) * 100 : 0,
|
|
648
|
+
avgHoldTime: avgHoldMs > 0 ? formatDuration(avgHoldMs) : null,
|
|
649
|
+
avgHoldTimeMs: avgHoldMs,
|
|
650
|
+
totalNetPnl,
|
|
651
|
+
totalFundingIncome: totalFunding,
|
|
652
|
+
totalFees,
|
|
653
|
+
bestTrade: bestTrade ? { symbol: bestTrade.symbol, netReturn: bestTrade.netReturn } : null,
|
|
654
|
+
worstTrade: worstTrade ? { symbol: worstTrade.symbol, netReturn: worstTrade.netReturn } : null,
|
|
655
|
+
avgEntrySpread: enhanced.avgEntrySpread,
|
|
656
|
+
avgExitSpread: enhanced.avgExitSpread,
|
|
657
|
+
avgSpreadDecay: enhanced.avgSpreadDecay,
|
|
658
|
+
byExchangePair: enhanced.byExchangePair,
|
|
659
|
+
byTimeOfDay: enhanced.byTimeOfDay,
|
|
660
|
+
optimalHoldTime: enhanced.optimalHoldTime,
|
|
661
|
+
};
|
|
662
|
+
if (isJson()) {
|
|
663
|
+
return printJson(jsonOk({ trades, summary, period: periodDays }));
|
|
664
|
+
}
|
|
665
|
+
// Display trade history table
|
|
666
|
+
if (trades.length > 0) {
|
|
667
|
+
console.log(chalk.cyan.bold(" Trade History\n"));
|
|
668
|
+
const rows = trades.map(t => {
|
|
669
|
+
const statusIcon = t.status === "completed" ? chalk.green("DONE")
|
|
670
|
+
: t.status === "open" ? chalk.yellow("OPEN")
|
|
671
|
+
: chalk.red("FAIL");
|
|
672
|
+
const entryDate = new Date(t.entryDate).toLocaleDateString();
|
|
673
|
+
const exitDate = t.exitDate ? new Date(t.exitDate).toLocaleDateString() : "-";
|
|
674
|
+
return [
|
|
675
|
+
chalk.white.bold(t.symbol),
|
|
676
|
+
t.exchanges,
|
|
677
|
+
entryDate,
|
|
678
|
+
exitDate,
|
|
679
|
+
t.holdDuration,
|
|
680
|
+
t.entrySpread !== null ? `${t.entrySpread.toFixed(1)}%` : "-",
|
|
681
|
+
t.size,
|
|
682
|
+
formatPnl(t.grossReturn),
|
|
683
|
+
chalk.yellow(`$${t.fundingIncome.toFixed(4)}`),
|
|
684
|
+
chalk.red(`-$${t.fees.toFixed(4)}`),
|
|
685
|
+
formatPnl(t.netReturn),
|
|
686
|
+
statusIcon,
|
|
687
|
+
t.exitReason ?? "-",
|
|
688
|
+
];
|
|
689
|
+
});
|
|
690
|
+
console.log(makeTable(["Symbol", "Exchanges", "Entry", "Exit", "Hold", "Spread", "Size", "Gross", "Funding", "Fees", "Net", "Status", "Reason"], rows));
|
|
691
|
+
}
|
|
692
|
+
// Summary
|
|
693
|
+
console.log(chalk.cyan.bold("\n Summary Statistics\n"));
|
|
694
|
+
console.log(` Period: Last ${periodDays} days`);
|
|
695
|
+
console.log(` Total trades: ${summary.totalTrades} (${summary.completedTrades} completed, ${summary.openTrades} open, ${summary.failedTrades} failed)`);
|
|
696
|
+
console.log(` Win rate: ${summary.winRate.toFixed(1)}%`);
|
|
697
|
+
console.log(` Avg hold time: ${summary.avgHoldTime ?? "-"}`);
|
|
698
|
+
console.log(` Total net PnL: ${formatPnl(summary.totalNetPnl)}`);
|
|
699
|
+
console.log(` Total funding: ${chalk.yellow(`$${summary.totalFundingIncome.toFixed(4)}`)}`);
|
|
700
|
+
console.log(` Total fees: ${chalk.red(`-$${summary.totalFees.toFixed(4)}`)}`);
|
|
701
|
+
if (summary.bestTrade) {
|
|
702
|
+
console.log(` Best trade: ${summary.bestTrade.symbol} ${formatPnl(summary.bestTrade.netReturn)}`);
|
|
703
|
+
}
|
|
704
|
+
if (summary.worstTrade) {
|
|
705
|
+
console.log(` Worst trade: ${summary.worstTrade.symbol} ${formatPnl(summary.worstTrade.netReturn)}`);
|
|
706
|
+
}
|
|
707
|
+
// ── Exchange Pair Performance ──
|
|
708
|
+
if (enhanced.byExchangePair.length > 0) {
|
|
709
|
+
console.log(chalk.cyan.bold("\n Exchange Pair Performance\n"));
|
|
710
|
+
const pairRows = enhanced.byExchangePair.map(p => [
|
|
711
|
+
chalk.white.bold(p.pair),
|
|
712
|
+
String(p.trades),
|
|
713
|
+
`${p.winRate.toFixed(0)}%`,
|
|
714
|
+
formatPnl(p.avgNetPnl),
|
|
715
|
+
p.avgHoldTime,
|
|
716
|
+
]);
|
|
717
|
+
console.log(makeTable(["Pair", "Trades", "Win%", "Avg PnL", "Avg Hold"], pairRows));
|
|
718
|
+
}
|
|
719
|
+
// ── Time of Day Performance ──
|
|
720
|
+
if (enhanced.byTimeOfDay.length > 0) {
|
|
721
|
+
console.log(chalk.cyan.bold("\n Time of Day Performance\n"));
|
|
722
|
+
const todRows = enhanced.byTimeOfDay.map(b => [
|
|
723
|
+
chalk.white(b.bucket),
|
|
724
|
+
String(b.trades),
|
|
725
|
+
`${b.winRate.toFixed(0)}%`,
|
|
726
|
+
formatPnl(b.avgNetPnl),
|
|
727
|
+
]);
|
|
728
|
+
console.log(makeTable(["UTC", "Trades", "Win%", "Avg PnL"], todRows));
|
|
729
|
+
}
|
|
730
|
+
// ── Spread Decay & Optimal Hold ──
|
|
731
|
+
if (enhanced.optimalHoldTime) {
|
|
732
|
+
console.log(` Optimal hold time: ~${enhanced.optimalHoldTime} (median of winning trades)`);
|
|
733
|
+
}
|
|
734
|
+
if (enhanced.avgEntrySpread > 0 && enhanced.avgExitSpread >= 0) {
|
|
735
|
+
console.log(` Avg spread decay: ${enhanced.avgEntrySpread.toFixed(1)}% -> ${enhanced.avgExitSpread.toFixed(1)}% over avg ${summary.avgHoldTime ?? "-"}`);
|
|
736
|
+
}
|
|
737
|
+
console.log();
|
|
738
|
+
});
|
|
739
|
+
// ── arb rebalance ──
|
|
740
|
+
arb
|
|
741
|
+
.command("rebalance")
|
|
742
|
+
.description("Cross-exchange balance rebalancing for arb")
|
|
743
|
+
.option("--check", "Show current balance distribution")
|
|
744
|
+
.option("--target <ratio>", "Target distribution ratio (e.g., '50:50' for 2 exchanges, '33:33:33' for 3)")
|
|
745
|
+
.option("--amount <usd>", "Total amount to rebalance")
|
|
746
|
+
.option("--dry-run", "Show plan without executing")
|
|
747
|
+
.option("--exchanges <list>", "Comma-separated exchanges", "lighter,pacifica,hyperliquid")
|
|
748
|
+
.action(async (opts) => {
|
|
749
|
+
const exchangeNames = opts.exchanges.split(",").map(e => e.trim());
|
|
750
|
+
const adapters = new Map();
|
|
751
|
+
for (const name of exchangeNames) {
|
|
752
|
+
try {
|
|
753
|
+
adapters.set(name, await getAdapterForExchange(name));
|
|
754
|
+
}
|
|
755
|
+
catch { /* skip unavailable */ }
|
|
756
|
+
}
|
|
757
|
+
if (adapters.size === 0) {
|
|
758
|
+
if (isJson())
|
|
759
|
+
return printJson(jsonOk({ error: "No exchanges available" }));
|
|
760
|
+
console.error(chalk.red("\n No exchanges available. Check credentials.\n"));
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
// Default to --check if no action specified
|
|
764
|
+
if (!opts.target && !opts.check) {
|
|
765
|
+
opts.check = true;
|
|
766
|
+
}
|
|
767
|
+
const snapshots = await fetchAllBalances(adapters);
|
|
768
|
+
const totalEquity = snapshots.reduce((s, e) => s + e.equity, 0);
|
|
769
|
+
const totalAvailable = snapshots.reduce((s, e) => s + e.available, 0);
|
|
770
|
+
const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : e.toUpperCase();
|
|
771
|
+
const exChain = (e) => e === "pacifica" ? "Solana" : e === "hyperliquid" ? "Hyperliquid" : e === "lighter" ? "Arbitrum" : "unknown";
|
|
772
|
+
if (opts.check) {
|
|
773
|
+
// Show current balance distribution
|
|
774
|
+
if (isJson()) {
|
|
775
|
+
return printJson(jsonOk({
|
|
776
|
+
balances: snapshots.map(s => ({
|
|
777
|
+
exchange: s.exchange,
|
|
778
|
+
abbr: exAbbr(s.exchange),
|
|
779
|
+
chain: exChain(s.exchange),
|
|
780
|
+
equity: s.equity,
|
|
781
|
+
available: s.available,
|
|
782
|
+
marginUsed: s.marginUsed,
|
|
783
|
+
unrealizedPnl: s.unrealizedPnl,
|
|
784
|
+
allocationPct: totalEquity > 0 ? (s.equity / totalEquity) * 100 : 0,
|
|
785
|
+
})),
|
|
786
|
+
totalEquity,
|
|
787
|
+
totalAvailable,
|
|
788
|
+
}));
|
|
789
|
+
}
|
|
790
|
+
console.log(chalk.cyan("\n Cross-Exchange Balance Distribution\n"));
|
|
791
|
+
const rows = snapshots.map(s => {
|
|
792
|
+
const pct = totalEquity > 0 ? ((s.equity / totalEquity) * 100).toFixed(1) : "0.0";
|
|
793
|
+
return [
|
|
794
|
+
chalk.white.bold(exAbbr(s.exchange).padEnd(4)),
|
|
795
|
+
chalk.gray(exChain(s.exchange).padEnd(12)),
|
|
796
|
+
`$${formatUsd(s.equity)}`,
|
|
797
|
+
`$${formatUsd(s.available)}`,
|
|
798
|
+
`$${formatUsd(s.marginUsed)}`,
|
|
799
|
+
s.unrealizedPnl >= 0
|
|
800
|
+
? chalk.green(`+$${formatUsd(s.unrealizedPnl)}`)
|
|
801
|
+
: chalk.red(`-$${formatUsd(Math.abs(s.unrealizedPnl))}`),
|
|
802
|
+
`${pct}%`,
|
|
803
|
+
];
|
|
804
|
+
});
|
|
805
|
+
console.log(makeTable(["Exch", "Chain", "Equity", "Available", "Margin", "uPnL", "Alloc%"], rows));
|
|
806
|
+
console.log(chalk.cyan.bold(" Totals"));
|
|
807
|
+
console.log(` Total Equity: $${formatUsd(totalEquity)}`);
|
|
808
|
+
console.log(` Total Available: $${formatUsd(totalAvailable)}`);
|
|
809
|
+
console.log(` Exchanges: ${snapshots.length}\n`);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
// Parse target ratios
|
|
813
|
+
if (!opts.target) {
|
|
814
|
+
console.error(chalk.red(" --target required (e.g., '50:50' or '33:33:33')"));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
const ratios = opts.target.split(":").map(Number);
|
|
818
|
+
if (ratios.length !== snapshots.length || ratios.some(isNaN)) {
|
|
819
|
+
console.error(chalk.red(` Target ratio must have ${snapshots.length} parts (one per exchange), got '${opts.target}'`));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const ratioSum = ratios.reduce((a, b) => a + b, 0);
|
|
823
|
+
const weights = {};
|
|
824
|
+
snapshots.forEach((s, i) => {
|
|
825
|
+
weights[s.exchange] = ratios[i] / ratioSum;
|
|
826
|
+
});
|
|
827
|
+
// Compute plan
|
|
828
|
+
const plan = computeRebalancePlan(snapshots, { weights, minMove: 10, reserve: 10 });
|
|
829
|
+
if (plan.moves.length === 0) {
|
|
830
|
+
if (isJson())
|
|
831
|
+
return printJson(jsonOk({ status: "balanced", moves: [], snapshots }));
|
|
832
|
+
console.log(chalk.green("\n Already balanced -- no moves needed.\n"));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
// If --amount specified, scale moves proportionally
|
|
836
|
+
let moves = plan.moves;
|
|
837
|
+
if (opts.amount) {
|
|
838
|
+
const requestedAmount = parseFloat(opts.amount);
|
|
839
|
+
const totalMoveAmount = moves.reduce((s, m) => s + m.amount, 0);
|
|
840
|
+
if (totalMoveAmount > 0) {
|
|
841
|
+
const scale = Math.min(1, requestedAmount / totalMoveAmount);
|
|
842
|
+
moves = moves.map(m => ({ ...m, amount: Math.floor(m.amount * scale) })).filter(m => m.amount >= 10);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// Get bridge route info for each move
|
|
846
|
+
const moveDetails = await Promise.all(moves.map(async (m) => {
|
|
847
|
+
const srcChain = EXCHANGE_TO_CHAIN[m.from] ?? "unknown";
|
|
848
|
+
const dstChain = EXCHANGE_TO_CHAIN[m.to] ?? "unknown";
|
|
849
|
+
let bridgeFee = 0;
|
|
850
|
+
let bridgeProvider = "same-chain";
|
|
851
|
+
let bridgeTime = "instant";
|
|
852
|
+
if (srcChain !== dstChain && srcChain !== "unknown" && dstChain !== "unknown") {
|
|
853
|
+
try {
|
|
854
|
+
const quote = await getBestQuote(srcChain, dstChain, m.amount, "0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000");
|
|
855
|
+
bridgeFee = quote.fee;
|
|
856
|
+
bridgeProvider = quote.provider;
|
|
857
|
+
bridgeTime = `~${Math.ceil(quote.estimatedTime / 60)}min`;
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
bridgeFee = 0.5; // fallback estimate
|
|
861
|
+
bridgeProvider = "cctp";
|
|
862
|
+
bridgeTime = "~3min";
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
...m,
|
|
867
|
+
srcChain,
|
|
868
|
+
dstChain,
|
|
869
|
+
bridgeFee,
|
|
870
|
+
bridgeProvider,
|
|
871
|
+
bridgeTime,
|
|
872
|
+
};
|
|
873
|
+
}));
|
|
874
|
+
if (isJson()) {
|
|
875
|
+
return printJson(jsonOk({
|
|
876
|
+
status: opts.dryRun ? "dry_run" : "planned",
|
|
877
|
+
target: weights,
|
|
878
|
+
moves: moveDetails,
|
|
879
|
+
snapshots,
|
|
880
|
+
totalEquity,
|
|
881
|
+
}));
|
|
882
|
+
}
|
|
883
|
+
console.log(chalk.cyan("\n Rebalance Plan\n"));
|
|
884
|
+
// Show current vs target
|
|
885
|
+
const stateRows = snapshots.map(s => {
|
|
886
|
+
const targetPct = (weights[s.exchange] * 100).toFixed(1);
|
|
887
|
+
const currentPct = totalEquity > 0 ? ((s.equity / totalEquity) * 100).toFixed(1) : "0.0";
|
|
888
|
+
const targetUsd = totalAvailable * weights[s.exchange];
|
|
889
|
+
const diff = s.available - targetUsd;
|
|
890
|
+
const diffStr = diff >= 0
|
|
891
|
+
? chalk.green(`+$${formatUsd(diff)}`)
|
|
892
|
+
: chalk.red(`-$${formatUsd(Math.abs(diff))}`);
|
|
893
|
+
return [
|
|
894
|
+
chalk.white.bold(exAbbr(s.exchange)),
|
|
895
|
+
`$${formatUsd(s.available)}`,
|
|
896
|
+
`${currentPct}%`,
|
|
897
|
+
`$${formatUsd(targetUsd)}`,
|
|
898
|
+
`${targetPct}%`,
|
|
899
|
+
diffStr,
|
|
900
|
+
];
|
|
901
|
+
});
|
|
902
|
+
console.log(makeTable(["Exch", "Available", "Current%", "Target$", "Target%", "Diff"], stateRows));
|
|
903
|
+
// Show moves
|
|
904
|
+
console.log(chalk.cyan.bold("\n Transfers\n"));
|
|
905
|
+
for (let i = 0; i < moveDetails.length; i++) {
|
|
906
|
+
const m = moveDetails[i];
|
|
907
|
+
console.log(chalk.white.bold(` Move ${i + 1}: $${m.amount} ${exAbbr(m.from)} -> ${exAbbr(m.to)}`));
|
|
908
|
+
console.log(chalk.gray(` Route: ${m.srcChain} -> ${m.dstChain} via ${m.bridgeProvider}`));
|
|
909
|
+
console.log(chalk.gray(` Fee: ~$${m.bridgeFee.toFixed(2)} | Time: ${m.bridgeTime}`));
|
|
910
|
+
console.log();
|
|
911
|
+
}
|
|
912
|
+
const totalFees = moveDetails.reduce((s, m) => s + m.bridgeFee, 0);
|
|
913
|
+
console.log(chalk.gray(` Total bridge fees: ~$${totalFees.toFixed(2)}`));
|
|
914
|
+
if (opts.dryRun) {
|
|
915
|
+
console.log(chalk.yellow("\n [DRY RUN] No transfers executed.\n"));
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
console.log(chalk.yellow("\n To execute, use: perp rebalance execute --auto-bridge\n"));
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
}
|