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,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live Dashboard Server — HTTP + WebSocket for real-time portfolio monitoring.
|
|
3
|
+
*
|
|
4
|
+
* Polls all configured exchange adapters and pushes updates to connected clients.
|
|
5
|
+
* Includes cross-exchange arb data: funding rate comparison + dex arb scan.
|
|
6
|
+
*/
|
|
7
|
+
import { createServer } from "http";
|
|
8
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
9
|
+
import { getUI } from "./ui.js";
|
|
10
|
+
import { WsFeedManager } from "./ws-feeds.js";
|
|
11
|
+
// Cached arb data (polled less frequently)
|
|
12
|
+
let cachedArb = { opportunities: [], dexArb: [], dexAssets: [], dexNames: [], exchangeStatus: {} };
|
|
13
|
+
// Cached market data (polled with arb cycle — market metadata rarely changes)
|
|
14
|
+
const cachedMarkets = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Find an available port starting from the given port.
|
|
17
|
+
*/
|
|
18
|
+
async function findPort(startPort) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const srv = createServer();
|
|
21
|
+
srv.listen(startPort, () => {
|
|
22
|
+
const addr = srv.address();
|
|
23
|
+
const port = typeof addr === "object" && addr ? addr.port : startPort;
|
|
24
|
+
srv.close(() => resolve(port));
|
|
25
|
+
});
|
|
26
|
+
srv.on("error", (err) => {
|
|
27
|
+
if (err.code === "EADDRINUSE") {
|
|
28
|
+
resolve(findPort(startPort + 1));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
reject(err);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Fetch arb data: cross-exchange funding rates + HIP-3 dex arb.
|
|
38
|
+
*/
|
|
39
|
+
async function pollArbData() {
|
|
40
|
+
const opportunities = [];
|
|
41
|
+
const dexArb = [];
|
|
42
|
+
let exchangeStatus = {};
|
|
43
|
+
try {
|
|
44
|
+
const { fetchAllFundingRates } = await import("../funding/rates.js");
|
|
45
|
+
const snapshot = await fetchAllFundingRates({ minSpread: 5 });
|
|
46
|
+
exchangeStatus = snapshot.exchangeStatus;
|
|
47
|
+
for (const sym of snapshot.symbols.slice(0, 20)) {
|
|
48
|
+
opportunities.push({
|
|
49
|
+
symbol: sym.symbol,
|
|
50
|
+
longExchange: sym.longExchange,
|
|
51
|
+
shortExchange: sym.shortExchange,
|
|
52
|
+
spreadAnnual: sym.maxSpreadAnnual,
|
|
53
|
+
estHourlyUsd: sym.estHourlyIncomeUsd,
|
|
54
|
+
rates: sym.rates.map((r) => ({
|
|
55
|
+
exchange: r.exchange,
|
|
56
|
+
annualizedPct: r.annualizedPct,
|
|
57
|
+
hourlyRate: r.hourlyRate,
|
|
58
|
+
markPrice: r.markPrice,
|
|
59
|
+
})),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// funding rates unavailable
|
|
65
|
+
}
|
|
66
|
+
const dexAssets = [];
|
|
67
|
+
const dexNamesSet = new Set();
|
|
68
|
+
try {
|
|
69
|
+
const { fetchAllDexAssets, findDexArbPairs } = await import("../dex-asset-map.js");
|
|
70
|
+
const { annualizeRate } = await import("../funding/normalize.js");
|
|
71
|
+
// Single fetch — used for both arb pairs and rate comparison table
|
|
72
|
+
const allAssets = await fetchAllDexAssets();
|
|
73
|
+
const pairs = findDexArbPairs(allAssets, { minAnnualSpread: 10 });
|
|
74
|
+
for (const p of pairs.slice(0, 15)) {
|
|
75
|
+
dexArb.push({
|
|
76
|
+
underlying: p.underlying,
|
|
77
|
+
longDex: `${p.long.dex}:${p.long.base}`,
|
|
78
|
+
shortDex: `${p.short.dex}:${p.short.base}`,
|
|
79
|
+
annualSpread: p.annualSpread,
|
|
80
|
+
priceGapPct: p.priceGapPct,
|
|
81
|
+
viability: p.viability,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const byBase = new Map();
|
|
85
|
+
for (const a of allAssets) {
|
|
86
|
+
dexNamesSet.add(a.dex);
|
|
87
|
+
if (!byBase.has(a.base))
|
|
88
|
+
byBase.set(a.base, []);
|
|
89
|
+
byBase.get(a.base).push(a);
|
|
90
|
+
}
|
|
91
|
+
// Only show assets on 2+ dexes, sorted by max spread
|
|
92
|
+
for (const [base, assets] of byBase) {
|
|
93
|
+
if (assets.length < 2)
|
|
94
|
+
continue;
|
|
95
|
+
const row = {
|
|
96
|
+
base,
|
|
97
|
+
dexes: assets.map((a) => {
|
|
98
|
+
// All dex funding rates are already per-hour, same as hyperliquid
|
|
99
|
+
const ann = annualizeRate(a.fundingRate, "hyperliquid");
|
|
100
|
+
return { dex: a.dex, rate: a.fundingRate, annualizedPct: ann, markPrice: a.markPrice, oi: a.openInterest };
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
dexAssets.push(row);
|
|
104
|
+
}
|
|
105
|
+
// Sort by max spread across dexes
|
|
106
|
+
dexAssets.sort((a, b) => {
|
|
107
|
+
const spreadA = Math.max(...a.dexes.map((d) => d.annualizedPct)) - Math.min(...a.dexes.map((d) => d.annualizedPct));
|
|
108
|
+
const spreadB = Math.max(...b.dexes.map((d) => d.annualizedPct)) - Math.min(...b.dexes.map((d) => d.annualizedPct));
|
|
109
|
+
return spreadB - spreadA;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// dex arb unavailable
|
|
114
|
+
}
|
|
115
|
+
const dexNames = [...dexNamesSet].sort();
|
|
116
|
+
return { opportunities, dexArb, dexAssets: dexAssets.slice(0, 30), dexNames, exchangeStatus };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Poll all exchanges and return a unified snapshot.
|
|
120
|
+
*/
|
|
121
|
+
/** Poll market data for all exchanges (called on arb cycle, not every 5s) */
|
|
122
|
+
async function pollMarkets(exchanges) {
|
|
123
|
+
await Promise.allSettled(exchanges.map(async (ex) => {
|
|
124
|
+
try {
|
|
125
|
+
const markets = await ex.adapter.getMarkets();
|
|
126
|
+
cachedMarkets.set(ex.name, markets.slice(0, 10));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// keep previous cached data
|
|
130
|
+
}
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
/** Build snapshot from WsFeedManager state (no REST calls needed) */
|
|
134
|
+
function buildSnapshotFromFeeds(feedMgr, exchanges) {
|
|
135
|
+
const states = feedMgr.getAllStates();
|
|
136
|
+
let exchangeData = [];
|
|
137
|
+
for (const ex of exchanges) {
|
|
138
|
+
const state = states.get(ex.name);
|
|
139
|
+
const emptyBal = { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
|
|
140
|
+
exchangeData.push({
|
|
141
|
+
name: ex.name,
|
|
142
|
+
balance: state?.balance ?? emptyBal,
|
|
143
|
+
positions: [...(state?.positions ?? [])], // copy — mergeAndTotal mutates
|
|
144
|
+
orders: [...(state?.orders ?? [])],
|
|
145
|
+
topMarkets: cachedMarkets.get(ex.name) ?? [],
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return mergeAndTotal(exchangeData);
|
|
149
|
+
}
|
|
150
|
+
/** Legacy REST-based snapshot (used for /api/snapshot fallback) */
|
|
151
|
+
async function pollSnapshot(exchanges, feedMgr) {
|
|
152
|
+
// If feedMgr is available, use WS state directly (no API calls)
|
|
153
|
+
if (feedMgr) {
|
|
154
|
+
return buildSnapshotFromFeeds(feedMgr, exchanges);
|
|
155
|
+
}
|
|
156
|
+
const { withCache, TTL_ACCOUNT } = await import("../cache.js");
|
|
157
|
+
const emptyBalance = { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
|
|
158
|
+
const results = await Promise.allSettled(exchanges.map(async (ex) => {
|
|
159
|
+
const [balance, positions, orders] = await Promise.all([
|
|
160
|
+
withCache(`dash:${ex.name}:balance`, TTL_ACCOUNT, () => ex.adapter.getBalance()).catch(() => emptyBalance),
|
|
161
|
+
withCache(`dash:${ex.name}:positions`, TTL_ACCOUNT, () => ex.adapter.getPositions()).catch(() => []),
|
|
162
|
+
withCache(`dash:${ex.name}:orders`, TTL_ACCOUNT, () => ex.adapter.getOpenOrders()).catch(() => []),
|
|
163
|
+
]);
|
|
164
|
+
return { name: ex.name, balance, positions, orders, topMarkets: cachedMarkets.get(ex.name) ?? [] };
|
|
165
|
+
}));
|
|
166
|
+
let exchangeData = results
|
|
167
|
+
.filter((r) => r.status === "fulfilled")
|
|
168
|
+
.map((r) => r.value);
|
|
169
|
+
return mergeAndTotal(exchangeData);
|
|
170
|
+
}
|
|
171
|
+
/** Merge HL dex entries + compute totals */
|
|
172
|
+
function mergeAndTotal(exchangeData) {
|
|
173
|
+
// Merge hl:* dex entries into main hyperliquid (same wallet, dex pools are subsets of main balance)
|
|
174
|
+
const hlEntry = exchangeData.find(e => e.name === "hyperliquid");
|
|
175
|
+
if (hlEntry) {
|
|
176
|
+
const dexEntries = exchangeData.filter(e => e.name.startsWith("hl:") && e.name !== "hyperliquid");
|
|
177
|
+
// Main accountValue already includes dex pool funds — DON'T add dex balances (would double-count).
|
|
178
|
+
// Only merge positions/orders, and attach dex breakdown for UI display.
|
|
179
|
+
const dexBalances = [];
|
|
180
|
+
let dexPnlSum = 0;
|
|
181
|
+
for (const dex of dexEntries) {
|
|
182
|
+
const dexName = dex.name.replace("hl:", "");
|
|
183
|
+
dexBalances.push({ name: dexName, balance: { ...dex.balance } });
|
|
184
|
+
hlEntry.positions.push(...dex.positions);
|
|
185
|
+
hlEntry.orders.push(...dex.orders);
|
|
186
|
+
dexPnlSum += Number(dex.balance.unrealizedPnl) || 0;
|
|
187
|
+
}
|
|
188
|
+
// Add dex unrealizedPnl to main HL balance (equity already includes dex funds, but PnL doesn't)
|
|
189
|
+
if (dexPnlSum !== 0) {
|
|
190
|
+
const hlPnl = Number(hlEntry.balance.unrealizedPnl) || 0;
|
|
191
|
+
hlEntry.balance = { ...hlEntry.balance, unrealizedPnl: String(hlPnl + dexPnlSum) };
|
|
192
|
+
}
|
|
193
|
+
if (dexEntries.length > 0) {
|
|
194
|
+
hlEntry.dexBalances = dexBalances;
|
|
195
|
+
}
|
|
196
|
+
exchangeData = exchangeData.filter(e => !e.name.startsWith("hl:") || e.name === "hyperliquid");
|
|
197
|
+
}
|
|
198
|
+
const totals = {
|
|
199
|
+
equity: 0,
|
|
200
|
+
available: 0,
|
|
201
|
+
marginUsed: 0,
|
|
202
|
+
unrealizedPnl: 0,
|
|
203
|
+
positionCount: 0,
|
|
204
|
+
orderCount: 0,
|
|
205
|
+
};
|
|
206
|
+
for (const ex of exchangeData) {
|
|
207
|
+
totals.equity += Number(ex.balance.equity) || 0;
|
|
208
|
+
totals.available += Number(ex.balance.available) || 0;
|
|
209
|
+
totals.marginUsed += Number(ex.balance.marginUsed) || 0;
|
|
210
|
+
totals.unrealizedPnl += Number(ex.balance.unrealizedPnl) || 0;
|
|
211
|
+
totals.positionCount += ex.positions.length;
|
|
212
|
+
totals.orderCount += ex.orders.length;
|
|
213
|
+
}
|
|
214
|
+
return { timestamp: new Date().toISOString(), exchanges: exchangeData, totals, arb: cachedArb };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Broadcast a message to all connected WebSocket clients.
|
|
218
|
+
*/
|
|
219
|
+
function broadcast(wss, data) {
|
|
220
|
+
const msg = JSON.stringify(data);
|
|
221
|
+
for (const client of wss.clients) {
|
|
222
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
223
|
+
client.send(msg);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Start the dashboard HTTP + WebSocket server.
|
|
229
|
+
*/
|
|
230
|
+
export async function startDashboard(exchanges, opts = {}) {
|
|
231
|
+
const arbInterval = opts.arbInterval ?? 30000;
|
|
232
|
+
const requestedPort = opts.port ?? 3456;
|
|
233
|
+
const port = await findPort(requestedPort);
|
|
234
|
+
const html = getUI();
|
|
235
|
+
let feedMgr;
|
|
236
|
+
const server = createServer((req, res) => {
|
|
237
|
+
if (req.url === "/" || req.url === "/index.html") {
|
|
238
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
239
|
+
res.end(html);
|
|
240
|
+
}
|
|
241
|
+
else if (req.url === "/api/snapshot") {
|
|
242
|
+
pollSnapshot(exchanges, feedMgr).then((snap) => {
|
|
243
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
244
|
+
res.end(JSON.stringify(snap));
|
|
245
|
+
}).catch((err) => {
|
|
246
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
247
|
+
res.end(JSON.stringify({ error: String(err) }));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
res.writeHead(404);
|
|
252
|
+
res.end("Not found");
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
const wss = new WebSocketServer({ server });
|
|
256
|
+
const hasClients = () => wss.clients.size > 0;
|
|
257
|
+
// ── WS Feed Manager: real-time account data via exchange WS APIs ──
|
|
258
|
+
let broadcastTimer = null;
|
|
259
|
+
const BROADCAST_DEBOUNCE_MS = 1000;
|
|
260
|
+
feedMgr = new WsFeedManager(exchanges, {
|
|
261
|
+
onUpdate: (_exchange, _state) => {
|
|
262
|
+
// Debounce broadcasts: WS feeds fire rapidly, limit to 1/sec
|
|
263
|
+
if (broadcastTimer || !hasClients())
|
|
264
|
+
return;
|
|
265
|
+
broadcastTimer = setTimeout(() => {
|
|
266
|
+
broadcastTimer = null;
|
|
267
|
+
if (!hasClients())
|
|
268
|
+
return;
|
|
269
|
+
const snap = buildSnapshotFromFeeds(feedMgr, exchanges);
|
|
270
|
+
broadcast(wss, { type: "snapshot", data: snap });
|
|
271
|
+
}, BROADCAST_DEBOUNCE_MS);
|
|
272
|
+
},
|
|
273
|
+
signal: opts.signal,
|
|
274
|
+
});
|
|
275
|
+
// Send initial snapshot on connect
|
|
276
|
+
wss.on("connection", async (ws) => {
|
|
277
|
+
try {
|
|
278
|
+
const snap = await pollSnapshot(exchanges, feedMgr);
|
|
279
|
+
ws.send(JSON.stringify({ type: "snapshot", data: snap }));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// ignore
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
// Arb + market data: stays on REST polling (cross-exchange aggregation, no single WS covers it)
|
|
286
|
+
let arbTimer = null;
|
|
287
|
+
const startArbPolling = () => {
|
|
288
|
+
const pollArbAndMarkets = async () => {
|
|
289
|
+
try {
|
|
290
|
+
const [arbResult] = await Promise.allSettled([
|
|
291
|
+
pollArbData(),
|
|
292
|
+
pollMarkets(exchanges),
|
|
293
|
+
]);
|
|
294
|
+
if (arbResult.status === "fulfilled") {
|
|
295
|
+
cachedArb = arbResult.value;
|
|
296
|
+
}
|
|
297
|
+
if (hasClients()) {
|
|
298
|
+
broadcast(wss, { type: "arb", data: cachedArb });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
// ignore
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
pollArbAndMarkets(); // initial fetch
|
|
306
|
+
arbTimer = setInterval(pollArbAndMarkets, arbInterval);
|
|
307
|
+
};
|
|
308
|
+
// Handle abort signal
|
|
309
|
+
if (opts.signal) {
|
|
310
|
+
opts.signal.addEventListener("abort", () => {
|
|
311
|
+
if (broadcastTimer)
|
|
312
|
+
clearTimeout(broadcastTimer);
|
|
313
|
+
if (arbTimer)
|
|
314
|
+
clearInterval(arbTimer);
|
|
315
|
+
feedMgr?.close();
|
|
316
|
+
wss.close();
|
|
317
|
+
server.close();
|
|
318
|
+
}, { once: true });
|
|
319
|
+
}
|
|
320
|
+
return new Promise((resolve) => {
|
|
321
|
+
server.listen(port, async () => {
|
|
322
|
+
// Start WS feeds (connects to exchange WS APIs)
|
|
323
|
+
await feedMgr.start();
|
|
324
|
+
// Start arb REST polling (30s cycle)
|
|
325
|
+
startArbPolling();
|
|
326
|
+
resolve({
|
|
327
|
+
port,
|
|
328
|
+
close: () => {
|
|
329
|
+
if (broadcastTimer)
|
|
330
|
+
clearTimeout(broadcastTimer);
|
|
331
|
+
if (arbTimer)
|
|
332
|
+
clearInterval(arbTimer);
|
|
333
|
+
feedMgr?.close();
|
|
334
|
+
wss.close();
|
|
335
|
+
server.close();
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
}
|