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,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Historical funding rate tracking.
|
|
3
|
+
*
|
|
4
|
+
* Stores funding rate snapshots as JSONL files in ~/.perp/funding-rates/
|
|
5
|
+
* organized by month (YYYY-MM.jsonl). Provides averaging and trend analysis
|
|
6
|
+
* over configurable time windows.
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, existsSync } from "node:fs";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
// ── Constants ──
|
|
12
|
+
const DATA_DIR = join(homedir(), ".perp", "funding-rates");
|
|
13
|
+
const DEDUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
14
|
+
const CLEANUP_MAX_AGE_DAYS = 30;
|
|
15
|
+
// ── Internal helpers ──
|
|
16
|
+
function ensureDataDir() {
|
|
17
|
+
mkdirSync(DATA_DIR, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
function getMonthKey(date) {
|
|
20
|
+
const y = date.getFullYear();
|
|
21
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
22
|
+
return `${y}-${m}`;
|
|
23
|
+
}
|
|
24
|
+
function getFilePath(monthKey) {
|
|
25
|
+
return join(DATA_DIR, `${monthKey}.jsonl`);
|
|
26
|
+
}
|
|
27
|
+
/** Read all entries from a JSONL file, returning [] if file doesn't exist. */
|
|
28
|
+
function readJsonlFile(filePath) {
|
|
29
|
+
if (!existsSync(filePath))
|
|
30
|
+
return [];
|
|
31
|
+
try {
|
|
32
|
+
const content = readFileSync(filePath, "utf-8");
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (const line of content.split("\n")) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed)
|
|
37
|
+
continue;
|
|
38
|
+
try {
|
|
39
|
+
entries.push(JSON.parse(trimmed));
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// skip malformed lines
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Append entries to a JSONL file. */
|
|
52
|
+
function appendToJsonl(filePath, entries) {
|
|
53
|
+
if (entries.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const lines = entries.map(e => JSON.stringify(e)).join("\n") + "\n";
|
|
56
|
+
try {
|
|
57
|
+
// Append to existing file or create new
|
|
58
|
+
const existing = existsSync(filePath) ? readFileSync(filePath, "utf-8") : "";
|
|
59
|
+
const needsNewline = existing.length > 0 && !existing.endsWith("\n");
|
|
60
|
+
writeFileSync(filePath, existing + (needsNewline ? "\n" : "") + lines);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// If append fails, try writing fresh
|
|
64
|
+
writeFileSync(filePath, lines);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Get entries across all relevant month files for a time range. */
|
|
68
|
+
function getEntriesInRange(startTime, endTime) {
|
|
69
|
+
ensureDataDir();
|
|
70
|
+
// Determine which month files to read
|
|
71
|
+
const monthKeys = new Set();
|
|
72
|
+
const cur = new Date(startTime);
|
|
73
|
+
while (cur <= endTime) {
|
|
74
|
+
monthKeys.add(getMonthKey(cur));
|
|
75
|
+
cur.setMonth(cur.getMonth() + 1);
|
|
76
|
+
cur.setDate(1); // reset to 1st to avoid day overflow
|
|
77
|
+
}
|
|
78
|
+
// Always include the end month
|
|
79
|
+
monthKeys.add(getMonthKey(endTime));
|
|
80
|
+
const allEntries = [];
|
|
81
|
+
for (const key of monthKeys) {
|
|
82
|
+
const filePath = getFilePath(key);
|
|
83
|
+
const entries = readJsonlFile(filePath);
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
const entryTime = new Date(entry.ts).getTime();
|
|
86
|
+
if (entryTime >= startTime.getTime() && entryTime <= endTime.getTime()) {
|
|
87
|
+
allEntries.push(entry);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return allEntries;
|
|
92
|
+
}
|
|
93
|
+
// ── Public API ──
|
|
94
|
+
/**
|
|
95
|
+
* Clean up JSONL files older than 30 days.
|
|
96
|
+
* Called automatically on startup / first save.
|
|
97
|
+
*/
|
|
98
|
+
let _cleanupDone = false;
|
|
99
|
+
export function cleanupOldFiles() {
|
|
100
|
+
if (_cleanupDone)
|
|
101
|
+
return;
|
|
102
|
+
_cleanupDone = true;
|
|
103
|
+
ensureDataDir();
|
|
104
|
+
const cutoff = new Date();
|
|
105
|
+
cutoff.setDate(cutoff.getDate() - CLEANUP_MAX_AGE_DAYS);
|
|
106
|
+
const cutoffKey = getMonthKey(cutoff);
|
|
107
|
+
try {
|
|
108
|
+
const files = readdirSync(DATA_DIR);
|
|
109
|
+
for (const file of files) {
|
|
110
|
+
if (!file.endsWith(".jsonl"))
|
|
111
|
+
continue;
|
|
112
|
+
const monthKey = file.replace(".jsonl", "");
|
|
113
|
+
// Compare YYYY-MM strings lexicographically
|
|
114
|
+
if (monthKey < cutoffKey) {
|
|
115
|
+
try {
|
|
116
|
+
unlinkSync(join(DATA_DIR, file));
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// ignore deletion errors
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Export for testing
|
|
129
|
+
export function _resetCleanupFlag() {
|
|
130
|
+
_cleanupDone = false;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Save current funding rates as a historical snapshot.
|
|
134
|
+
* Deduplicates: skips entries if same symbol+exchange was saved within 5 minutes.
|
|
135
|
+
*/
|
|
136
|
+
export function saveFundingSnapshot(rates) {
|
|
137
|
+
cleanupOldFiles();
|
|
138
|
+
ensureDataDir();
|
|
139
|
+
const now = new Date();
|
|
140
|
+
const monthKey = getMonthKey(now);
|
|
141
|
+
const filePath = getFilePath(monthKey);
|
|
142
|
+
const ts = now.toISOString();
|
|
143
|
+
// Read existing entries for dedup check (only current month file)
|
|
144
|
+
const existing = readJsonlFile(filePath);
|
|
145
|
+
// Build a map of latest timestamps for each symbol+exchange
|
|
146
|
+
const latestTs = new Map();
|
|
147
|
+
for (const entry of existing) {
|
|
148
|
+
const key = `${entry.symbol}:${entry.exchange}`;
|
|
149
|
+
const entryTime = new Date(entry.ts).getTime();
|
|
150
|
+
const current = latestTs.get(key) ?? 0;
|
|
151
|
+
if (entryTime > current)
|
|
152
|
+
latestTs.set(key, entryTime);
|
|
153
|
+
}
|
|
154
|
+
// Filter out rates that would be duplicates (within 5 min)
|
|
155
|
+
const nowMs = now.getTime();
|
|
156
|
+
const toSave = [];
|
|
157
|
+
for (const r of rates) {
|
|
158
|
+
if (!r.symbol)
|
|
159
|
+
continue;
|
|
160
|
+
const key = `${r.symbol.toUpperCase()}:${r.exchange}`;
|
|
161
|
+
const lastSaved = latestTs.get(key) ?? 0;
|
|
162
|
+
if (nowMs - lastSaved < DEDUP_INTERVAL_MS)
|
|
163
|
+
continue;
|
|
164
|
+
toSave.push({
|
|
165
|
+
ts,
|
|
166
|
+
symbol: r.symbol.toUpperCase(),
|
|
167
|
+
exchange: r.exchange,
|
|
168
|
+
rate: r.fundingRate,
|
|
169
|
+
hourlyRate: r.hourlyRate,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
appendToJsonl(filePath, toSave);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Get average funding rate for a symbol+exchange over the last N hours.
|
|
176
|
+
* Returns null if no data available.
|
|
177
|
+
*/
|
|
178
|
+
export function getAvgFundingRate(symbol, exchange, hours) {
|
|
179
|
+
const endTime = new Date();
|
|
180
|
+
const startTime = new Date(endTime.getTime() - hours * 60 * 60 * 1000);
|
|
181
|
+
const entries = getEntriesInRange(startTime, endTime);
|
|
182
|
+
const filtered = entries.filter(e => e.symbol === symbol.toUpperCase() && e.exchange === exchange.toLowerCase());
|
|
183
|
+
if (filtered.length === 0)
|
|
184
|
+
return null;
|
|
185
|
+
const sum = filtered.reduce((acc, e) => acc + e.hourlyRate, 0);
|
|
186
|
+
return sum / filtered.length;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get all historical rates for a symbol+exchange in a time range.
|
|
190
|
+
*/
|
|
191
|
+
export function getHistoricalRates(symbol, exchange, startTime, endTime) {
|
|
192
|
+
const entries = getEntriesInRange(startTime, endTime);
|
|
193
|
+
return entries
|
|
194
|
+
.filter(e => e.symbol === symbol.toUpperCase() && e.exchange === exchange.toLowerCase())
|
|
195
|
+
.map(e => ({ ts: e.ts, rate: e.rate, hourlyRate: e.hourlyRate }))
|
|
196
|
+
.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Get averaged rates for all symbols across time windows.
|
|
200
|
+
* Returns a Map with key format "SYMBOL:exchange".
|
|
201
|
+
*/
|
|
202
|
+
export function getHistoricalAverages(symbols, exchanges) {
|
|
203
|
+
const result = new Map();
|
|
204
|
+
const now = new Date();
|
|
205
|
+
// Read all entries from the last 7 days (covers all windows)
|
|
206
|
+
const startTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
207
|
+
const allEntries = getEntriesInRange(startTime, now);
|
|
208
|
+
// Group entries by symbol:exchange
|
|
209
|
+
const grouped = new Map();
|
|
210
|
+
for (const entry of allEntries) {
|
|
211
|
+
const key = `${entry.symbol}:${entry.exchange}`;
|
|
212
|
+
if (!grouped.has(key))
|
|
213
|
+
grouped.set(key, []);
|
|
214
|
+
grouped.get(key).push(entry);
|
|
215
|
+
}
|
|
216
|
+
const windows = [
|
|
217
|
+
{ key: "avg1h", ms: 1 * 60 * 60 * 1000 },
|
|
218
|
+
{ key: "avg8h", ms: 8 * 60 * 60 * 1000 },
|
|
219
|
+
{ key: "avg24h", ms: 24 * 60 * 60 * 1000 },
|
|
220
|
+
{ key: "avg7d", ms: 7 * 24 * 60 * 60 * 1000 },
|
|
221
|
+
];
|
|
222
|
+
for (const symbol of symbols) {
|
|
223
|
+
for (const exchange of exchanges) {
|
|
224
|
+
const key = `${symbol.toUpperCase()}:${exchange.toLowerCase()}`;
|
|
225
|
+
const entries = grouped.get(key) ?? [];
|
|
226
|
+
const avgs = { avg1h: null, avg8h: null, avg24h: null, avg7d: null };
|
|
227
|
+
for (const w of windows) {
|
|
228
|
+
const cutoff = now.getTime() - w.ms;
|
|
229
|
+
const inWindow = entries.filter(e => new Date(e.ts).getTime() >= cutoff);
|
|
230
|
+
if (inWindow.length > 0) {
|
|
231
|
+
const sum = inWindow.reduce((acc, e) => acc + e.hourlyRate, 0);
|
|
232
|
+
avgs[w.key] = sum / inWindow.length;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
result.set(key, avgs);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Calculate effective annualized return considering compounding frequency.
|
|
242
|
+
*
|
|
243
|
+
* All three main exchanges (HL, PAC, LT) compound every 1h (8760 times/year).
|
|
244
|
+
*
|
|
245
|
+
* Formula: (1 + rate)^(8760/compoundingHours) - 1
|
|
246
|
+
*
|
|
247
|
+
* @param hourlyRate - the per-hour funding rate
|
|
248
|
+
* @param compoundingHours - how often the exchange compounds (1 for all main exchanges)
|
|
249
|
+
* @returns effective annualized return as a decimal (not percentage)
|
|
250
|
+
*/
|
|
251
|
+
export function getCompoundedAnnualReturn(hourlyRate, compoundingHours) {
|
|
252
|
+
// The rate per compounding period
|
|
253
|
+
const periodRate = hourlyRate * compoundingHours;
|
|
254
|
+
// Number of compounding periods per year
|
|
255
|
+
const periodsPerYear = 8760 / compoundingHours;
|
|
256
|
+
// Compounded annual return
|
|
257
|
+
return Math.pow(1 + periodRate, periodsPerYear) - 1;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get the compounding hours for an exchange.
|
|
261
|
+
* All main exchanges (HL, PAC, LT) compound every 1h.
|
|
262
|
+
*/
|
|
263
|
+
export function getExchangeCompoundingHours(exchange) {
|
|
264
|
+
// All main exchanges compound hourly
|
|
265
|
+
return 1;
|
|
266
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding rate normalization.
|
|
3
|
+
*
|
|
4
|
+
* Exchange funding rate periods:
|
|
5
|
+
* - Hyperliquid: per 1 HOUR (settles every 1h)
|
|
6
|
+
* - Pacifica: per 1 HOUR (settles every 1h)
|
|
7
|
+
* - Lighter: per 8 HOURS (API returns 8h rate, settles every 1h)
|
|
8
|
+
*
|
|
9
|
+
* HIP-3 deployed dexes on Hyperliquid use 8h funding periods.
|
|
10
|
+
*
|
|
11
|
+
* To compare rates across exchanges, we normalize everything to
|
|
12
|
+
* a per-hour basis, then annualize from there.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Get funding period in hours for an exchange.
|
|
16
|
+
* HIP-3 deployed dexes (not in the map) default to 1h (API returns hourly).
|
|
17
|
+
* HL and PAC are 1h, Lighter API returns 8h rates.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getFundingHours(exchange: string): number;
|
|
20
|
+
/** Convert a raw funding rate to per-hour rate */
|
|
21
|
+
export declare function toHourlyRate(rate: number, exchange: string): number;
|
|
22
|
+
/** Annualize a raw rate from a specific exchange */
|
|
23
|
+
export declare function annualizeRate(rate: number, exchange: string): number;
|
|
24
|
+
/** Annualize an already-normalized hourly rate (no exchange conversion needed) */
|
|
25
|
+
export declare function annualizeHourlyRate(hourlyRate: number): number;
|
|
26
|
+
/**
|
|
27
|
+
* Compute annualized spread between two exchange rates.
|
|
28
|
+
* Normalizes both to hourly before comparing.
|
|
29
|
+
*/
|
|
30
|
+
export declare function computeAnnualSpread(rateA: number, exchangeA: string, rateB: number, exchangeB: string): number;
|
|
31
|
+
/**
|
|
32
|
+
* Estimate hourly funding payment for a position.
|
|
33
|
+
* @param rate - raw funding rate from the exchange
|
|
34
|
+
* @param exchange - exchange name
|
|
35
|
+
* @param positionUsd - position notional in USD
|
|
36
|
+
* @param side - "long" or "short" (longs pay positive rate, shorts receive)
|
|
37
|
+
* @returns hourly payment in USD (positive = you pay, negative = you receive)
|
|
38
|
+
*/
|
|
39
|
+
export declare function estimateHourlyFunding(rate: number, exchange: string, positionUsd: number, side: "long" | "short"): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Funding rate normalization.
|
|
3
|
+
*
|
|
4
|
+
* Exchange funding rate periods:
|
|
5
|
+
* - Hyperliquid: per 1 HOUR (settles every 1h)
|
|
6
|
+
* - Pacifica: per 1 HOUR (settles every 1h)
|
|
7
|
+
* - Lighter: per 8 HOURS (API returns 8h rate, settles every 1h)
|
|
8
|
+
*
|
|
9
|
+
* HIP-3 deployed dexes on Hyperliquid use 8h funding periods.
|
|
10
|
+
*
|
|
11
|
+
* To compare rates across exchanges, we normalize everything to
|
|
12
|
+
* a per-hour basis, then annualize from there.
|
|
13
|
+
*/
|
|
14
|
+
/** Funding periods per year by convention */
|
|
15
|
+
const HOURLY_PERIODS = 24 * 365; // 8760
|
|
16
|
+
/** How many hours each exchange's rate covers */
|
|
17
|
+
const EXCHANGE_FUNDING_HOURS = {
|
|
18
|
+
hyperliquid: 1,
|
|
19
|
+
pacifica: 1,
|
|
20
|
+
lighter: 8,
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Get funding period in hours for an exchange.
|
|
24
|
+
* HIP-3 deployed dexes (not in the map) default to 1h (API returns hourly).
|
|
25
|
+
* HL and PAC are 1h, Lighter API returns 8h rates.
|
|
26
|
+
*/
|
|
27
|
+
export function getFundingHours(exchange) {
|
|
28
|
+
return EXCHANGE_FUNDING_HOURS[exchange.toLowerCase()] ?? 1;
|
|
29
|
+
}
|
|
30
|
+
/** Convert a raw funding rate to per-hour rate */
|
|
31
|
+
export function toHourlyRate(rate, exchange) {
|
|
32
|
+
const hours = EXCHANGE_FUNDING_HOURS[exchange.toLowerCase()] ?? 1;
|
|
33
|
+
return rate / hours;
|
|
34
|
+
}
|
|
35
|
+
/** Annualize a raw rate from a specific exchange */
|
|
36
|
+
export function annualizeRate(rate, exchange) {
|
|
37
|
+
const hourlyRate = toHourlyRate(rate, exchange);
|
|
38
|
+
return hourlyRate * HOURLY_PERIODS * 100; // as percentage
|
|
39
|
+
}
|
|
40
|
+
/** Annualize an already-normalized hourly rate (no exchange conversion needed) */
|
|
41
|
+
export function annualizeHourlyRate(hourlyRate) {
|
|
42
|
+
return hourlyRate * HOURLY_PERIODS * 100; // as percentage
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compute annualized spread between two exchange rates.
|
|
46
|
+
* Normalizes both to hourly before comparing.
|
|
47
|
+
*/
|
|
48
|
+
export function computeAnnualSpread(rateA, exchangeA, rateB, exchangeB) {
|
|
49
|
+
const hourlyA = toHourlyRate(rateA, exchangeA);
|
|
50
|
+
const hourlyB = toHourlyRate(rateB, exchangeB);
|
|
51
|
+
return Math.abs(hourlyA - hourlyB) * HOURLY_PERIODS * 100;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Estimate hourly funding payment for a position.
|
|
55
|
+
* @param rate - raw funding rate from the exchange
|
|
56
|
+
* @param exchange - exchange name
|
|
57
|
+
* @param positionUsd - position notional in USD
|
|
58
|
+
* @param side - "long" or "short" (longs pay positive rate, shorts receive)
|
|
59
|
+
* @returns hourly payment in USD (positive = you pay, negative = you receive)
|
|
60
|
+
*/
|
|
61
|
+
export function estimateHourlyFunding(rate, exchange, positionUsd, side) {
|
|
62
|
+
const hourlyRate = toHourlyRate(rate, exchange);
|
|
63
|
+
// Long pays positive funding, short receives positive funding
|
|
64
|
+
const multiplier = side === "long" ? 1 : -1;
|
|
65
|
+
return hourlyRate * positionUsd * multiplier;
|
|
66
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time 3-DEX funding rate comparison.
|
|
3
|
+
*
|
|
4
|
+
* Fetches funding rates from Pacifica, Hyperliquid, and Lighter in parallel,
|
|
5
|
+
* normalizes them to comparable hourly rates, and identifies arbitrage
|
|
6
|
+
* opportunities across exchanges.
|
|
7
|
+
*/
|
|
8
|
+
import { type HistoricalAverages } from "./history.js";
|
|
9
|
+
export interface ExchangeFundingRate {
|
|
10
|
+
exchange: "pacifica" | "hyperliquid" | "lighter";
|
|
11
|
+
symbol: string;
|
|
12
|
+
fundingRate: number;
|
|
13
|
+
hourlyRate: number;
|
|
14
|
+
annualizedPct: number;
|
|
15
|
+
markPrice: number;
|
|
16
|
+
nextFundingTime?: number;
|
|
17
|
+
historicalAvg?: HistoricalAverages;
|
|
18
|
+
}
|
|
19
|
+
export interface SymbolFundingComparison {
|
|
20
|
+
symbol: string;
|
|
21
|
+
rates: ExchangeFundingRate[];
|
|
22
|
+
maxSpreadAnnual: number;
|
|
23
|
+
longExchange: string;
|
|
24
|
+
shortExchange: string;
|
|
25
|
+
bestMarkPrice: number;
|
|
26
|
+
estHourlyIncomeUsd: number;
|
|
27
|
+
}
|
|
28
|
+
export interface FundingRateSnapshot {
|
|
29
|
+
timestamp: string;
|
|
30
|
+
symbols: SymbolFundingComparison[];
|
|
31
|
+
exchangeStatus: Record<string, "ok" | "error">;
|
|
32
|
+
}
|
|
33
|
+
export declare const TOP_SYMBOLS: string[];
|
|
34
|
+
/**
|
|
35
|
+
* Fetch funding rates from all 3 DEXs in parallel, normalize, and compare.
|
|
36
|
+
* Returns rates sorted by max spread (descending).
|
|
37
|
+
*/
|
|
38
|
+
export declare function fetchAllFundingRates(opts?: {
|
|
39
|
+
symbols?: string[];
|
|
40
|
+
minSpread?: number;
|
|
41
|
+
}): Promise<FundingRateSnapshot>;
|
|
42
|
+
/**
|
|
43
|
+
* Fetch rates for a single symbol across all exchanges.
|
|
44
|
+
*/
|
|
45
|
+
export declare function fetchSymbolFundingRates(symbol: string): Promise<SymbolFundingComparison | null>;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-time 3-DEX funding rate comparison.
|
|
3
|
+
*
|
|
4
|
+
* Fetches funding rates from Pacifica, Hyperliquid, and Lighter in parallel,
|
|
5
|
+
* normalizes them to comparable hourly rates, and identifies arbitrage
|
|
6
|
+
* opportunities across exchanges.
|
|
7
|
+
*/
|
|
8
|
+
import { toHourlyRate, annualizeRate, computeAnnualSpread, estimateHourlyFunding } from "./normalize.js";
|
|
9
|
+
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, fetchLighterFundingRates as fetchLtFundingRates, } from "../shared-api.js";
|
|
10
|
+
import { getHistoricalAverages } from "./history.js";
|
|
11
|
+
// ── Default top symbols to track ──
|
|
12
|
+
export const TOP_SYMBOLS = [
|
|
13
|
+
"BTC", "ETH", "SOL", "DOGE", "SUI", "AVAX", "LINK", "ARB",
|
|
14
|
+
"WIF", "PEPE", "ONDO", "SEI", "TIA", "INJ", "NEAR",
|
|
15
|
+
"APT", "OP", "FIL", "AAVE", "MKR",
|
|
16
|
+
];
|
|
17
|
+
// ── Fetchers (using shared-api.ts) ──
|
|
18
|
+
async function fetchPacificaRates() {
|
|
19
|
+
try {
|
|
20
|
+
const assets = await fetchPacificaPrices();
|
|
21
|
+
return assets.map(p => {
|
|
22
|
+
const hourly = toHourlyRate(p.funding, "pacifica");
|
|
23
|
+
return {
|
|
24
|
+
exchange: "pacifica",
|
|
25
|
+
symbol: p.symbol,
|
|
26
|
+
fundingRate: p.funding,
|
|
27
|
+
hourlyRate: hourly,
|
|
28
|
+
annualizedPct: annualizeRate(p.funding, "pacifica"),
|
|
29
|
+
markPrice: p.mark,
|
|
30
|
+
nextFundingTime: p.nextFunding,
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function fetchHyperliquidRates() {
|
|
39
|
+
try {
|
|
40
|
+
const assets = await fetchHyperliquidMeta();
|
|
41
|
+
return assets.map(a => {
|
|
42
|
+
const hourly = toHourlyRate(a.funding, "hyperliquid");
|
|
43
|
+
return {
|
|
44
|
+
exchange: "hyperliquid",
|
|
45
|
+
symbol: a.symbol,
|
|
46
|
+
fundingRate: a.funding,
|
|
47
|
+
hourlyRate: hourly,
|
|
48
|
+
annualizedPct: annualizeRate(a.funding, "hyperliquid"),
|
|
49
|
+
markPrice: a.markPx,
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function fetchLighterRates() {
|
|
58
|
+
try {
|
|
59
|
+
const [details, funding] = await Promise.all([
|
|
60
|
+
fetchLighterOrderBookDetails(),
|
|
61
|
+
fetchLtFundingRates(),
|
|
62
|
+
]);
|
|
63
|
+
const priceMap = new Map(details.map(d => [d.marketId, d.lastTradePrice]));
|
|
64
|
+
const symMap = new Map(details.map(d => [d.marketId, d.symbol]));
|
|
65
|
+
return funding.map(fr => {
|
|
66
|
+
const symbol = fr.symbol || symMap.get(fr.marketId) || "";
|
|
67
|
+
const rate = fr.rate;
|
|
68
|
+
const hourly = toHourlyRate(rate, "lighter");
|
|
69
|
+
return {
|
|
70
|
+
exchange: "lighter",
|
|
71
|
+
symbol,
|
|
72
|
+
fundingRate: rate,
|
|
73
|
+
hourlyRate: hourly,
|
|
74
|
+
annualizedPct: annualizeRate(rate, "lighter"),
|
|
75
|
+
markPrice: fr.markPrice || priceMap.get(fr.marketId) || 0,
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ── Core comparison logic ──
|
|
84
|
+
/**
|
|
85
|
+
* Fetch funding rates from all 3 DEXs in parallel, normalize, and compare.
|
|
86
|
+
* Returns rates sorted by max spread (descending).
|
|
87
|
+
*/
|
|
88
|
+
export async function fetchAllFundingRates(opts) {
|
|
89
|
+
const exchangeStatus = {};
|
|
90
|
+
const [pacRates, hlRates, ltRates] = await Promise.all([
|
|
91
|
+
fetchPacificaRates().then(r => { exchangeStatus.pacifica = r.length > 0 ? "ok" : "error"; return r; }),
|
|
92
|
+
fetchHyperliquidRates().then(r => { exchangeStatus.hyperliquid = r.length > 0 ? "ok" : "error"; return r; }),
|
|
93
|
+
fetchLighterRates().then(r => { exchangeStatus.lighter = r.length > 0 ? "ok" : "error"; return r; }),
|
|
94
|
+
]);
|
|
95
|
+
const allRates = [...pacRates, ...hlRates, ...ltRates];
|
|
96
|
+
// Build per-symbol rate map
|
|
97
|
+
const rateMap = new Map();
|
|
98
|
+
for (const r of allRates) {
|
|
99
|
+
if (!r.symbol)
|
|
100
|
+
continue;
|
|
101
|
+
if (opts?.symbols && !opts.symbols.includes(r.symbol.toUpperCase()))
|
|
102
|
+
continue;
|
|
103
|
+
const key = r.symbol.toUpperCase();
|
|
104
|
+
if (!rateMap.has(key))
|
|
105
|
+
rateMap.set(key, []);
|
|
106
|
+
rateMap.get(key).push(r);
|
|
107
|
+
}
|
|
108
|
+
// Attach historical averages when available
|
|
109
|
+
try {
|
|
110
|
+
const symbols = Array.from(rateMap.keys());
|
|
111
|
+
const exchanges = ["pacifica", "hyperliquid", "lighter"];
|
|
112
|
+
const historicals = getHistoricalAverages(symbols, exchanges);
|
|
113
|
+
for (const [, rates] of rateMap) {
|
|
114
|
+
for (const r of rates) {
|
|
115
|
+
const key = `${r.symbol.toUpperCase()}:${r.exchange}`;
|
|
116
|
+
const avg = historicals.get(key);
|
|
117
|
+
if (avg)
|
|
118
|
+
r.historicalAvg = avg;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Non-critical
|
|
124
|
+
}
|
|
125
|
+
const comparisons = [];
|
|
126
|
+
const minSpread = opts?.minSpread ?? 0;
|
|
127
|
+
for (const [symbol, rates] of rateMap) {
|
|
128
|
+
// Need at least 2 exchanges to compare
|
|
129
|
+
if (rates.length < 2)
|
|
130
|
+
continue;
|
|
131
|
+
// Sort by hourly rate (ascending)
|
|
132
|
+
rates.sort((a, b) => a.hourlyRate - b.hourlyRate);
|
|
133
|
+
const lowest = rates[0];
|
|
134
|
+
const highest = rates[rates.length - 1];
|
|
135
|
+
const maxSpreadAnnual = computeAnnualSpread(highest.fundingRate, highest.exchange, lowest.fundingRate, lowest.exchange);
|
|
136
|
+
if (maxSpreadAnnual < minSpread)
|
|
137
|
+
continue;
|
|
138
|
+
// Best mark price: prefer HL (most liquid), then PAC, then LT
|
|
139
|
+
const hlRate = rates.find(r => r.exchange === "hyperliquid");
|
|
140
|
+
const pacRate = rates.find(r => r.exchange === "pacifica");
|
|
141
|
+
const ltRate = rates.find(r => r.exchange === "lighter");
|
|
142
|
+
const bestMarkPrice = hlRate?.markPrice || pacRate?.markPrice || ltRate?.markPrice || 0;
|
|
143
|
+
// Estimate hourly income for $1000 notional arb
|
|
144
|
+
const notional = 1000;
|
|
145
|
+
const longIncome = estimateHourlyFunding(lowest.fundingRate, lowest.exchange, notional, "long");
|
|
146
|
+
const shortIncome = estimateHourlyFunding(highest.fundingRate, highest.exchange, notional, "short");
|
|
147
|
+
const estHourlyIncomeUsd = -(longIncome + shortIncome); // negate because income = -cost
|
|
148
|
+
comparisons.push({
|
|
149
|
+
symbol,
|
|
150
|
+
rates,
|
|
151
|
+
maxSpreadAnnual,
|
|
152
|
+
longExchange: lowest.exchange, // long where funding is lowest
|
|
153
|
+
shortExchange: highest.exchange, // short where funding is highest
|
|
154
|
+
bestMarkPrice,
|
|
155
|
+
estHourlyIncomeUsd,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// Sort by spread descending
|
|
159
|
+
comparisons.sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual);
|
|
160
|
+
return {
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
symbols: comparisons,
|
|
163
|
+
exchangeStatus,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Fetch rates for a single symbol across all exchanges.
|
|
168
|
+
*/
|
|
169
|
+
export async function fetchSymbolFundingRates(symbol) {
|
|
170
|
+
const snapshot = await fetchAllFundingRates({ symbols: [symbol.toUpperCase()] });
|
|
171
|
+
return snapshot.symbols[0] ?? null;
|
|
172
|
+
}
|
package/dist/funding.js
ADDED
package/dist/index.d.ts
ADDED