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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the funding-rates module.
|
|
4
|
+
*
|
|
5
|
+
* These test the core comparison/normalization logic using mocked API responses.
|
|
6
|
+
* Integration tests that hit real APIs live in integration/.
|
|
7
|
+
*/
|
|
8
|
+
// Mock fetch globally before importing the module
|
|
9
|
+
const mockFetch = vi.fn();
|
|
10
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
11
|
+
// Import after mocking fetch
|
|
12
|
+
const { fetchAllFundingRates, fetchSymbolFundingRates, TOP_SYMBOLS } = await import("../funding-rates.js");
|
|
13
|
+
const { invalidateCache } = await import("../cache.js");
|
|
14
|
+
// ── Helpers to build mock API responses ──
|
|
15
|
+
function makePacificaResponse(rates) {
|
|
16
|
+
return { data: rates };
|
|
17
|
+
}
|
|
18
|
+
function makeHyperliquidResponse(assets, ctxs) {
|
|
19
|
+
return [{ universe: assets }, ctxs];
|
|
20
|
+
}
|
|
21
|
+
function makeLighterResponse(details, fundingRates) {
|
|
22
|
+
return {
|
|
23
|
+
details: { order_book_details: details },
|
|
24
|
+
funding: { funding_rates: fundingRates.map(fr => ({ exchange: "lighter", ...fr })) },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function setupMockFetch(opts) {
|
|
28
|
+
mockFetch.mockImplementation(async (url, init) => {
|
|
29
|
+
const urlStr = typeof url === "string" ? url : url.toString();
|
|
30
|
+
// Pacifica
|
|
31
|
+
if (urlStr.includes("pacifica.fi")) {
|
|
32
|
+
if (opts.pacError)
|
|
33
|
+
throw new Error("Pacifica API error");
|
|
34
|
+
return {
|
|
35
|
+
json: async () => makePacificaResponse(opts.pac ?? []),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Hyperliquid
|
|
39
|
+
if (urlStr.includes("hyperliquid.xyz")) {
|
|
40
|
+
if (opts.hlError)
|
|
41
|
+
throw new Error("HL API error");
|
|
42
|
+
const hl = opts.hl ?? { assets: [], ctxs: [] };
|
|
43
|
+
return {
|
|
44
|
+
json: async () => makeHyperliquidResponse(hl.assets, hl.ctxs),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Lighter - two endpoints
|
|
48
|
+
if (urlStr.includes("zklighter") && urlStr.includes("orderBookDetails")) {
|
|
49
|
+
if (opts.ltError)
|
|
50
|
+
throw new Error("Lighter API error");
|
|
51
|
+
const lt = opts.lt ?? { details: [], funding: [] };
|
|
52
|
+
return {
|
|
53
|
+
json: async () => ({ order_book_details: lt.details }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
if (urlStr.includes("zklighter") && urlStr.includes("funding-rates")) {
|
|
57
|
+
if (opts.ltError)
|
|
58
|
+
throw new Error("Lighter API error");
|
|
59
|
+
const lt = opts.lt ?? { details: [], funding: [] };
|
|
60
|
+
return {
|
|
61
|
+
json: async () => ({ funding_rates: lt.funding.map(fr => ({ exchange: "lighter", ...fr })) }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`Unexpected fetch: ${urlStr}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
mockFetch.mockReset();
|
|
69
|
+
invalidateCache();
|
|
70
|
+
});
|
|
71
|
+
// ──────────────────────────────────────────────
|
|
72
|
+
// TOP_SYMBOLS
|
|
73
|
+
// ──────────────────────────────────────────────
|
|
74
|
+
describe("TOP_SYMBOLS", () => {
|
|
75
|
+
it("includes major crypto assets", () => {
|
|
76
|
+
expect(TOP_SYMBOLS).toContain("BTC");
|
|
77
|
+
expect(TOP_SYMBOLS).toContain("ETH");
|
|
78
|
+
expect(TOP_SYMBOLS).toContain("SOL");
|
|
79
|
+
});
|
|
80
|
+
it("has at least 10 symbols", () => {
|
|
81
|
+
expect(TOP_SYMBOLS.length).toBeGreaterThanOrEqual(10);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
// fetchAllFundingRates
|
|
86
|
+
// ──────────────────────────────────────────────
|
|
87
|
+
describe("fetchAllFundingRates", () => {
|
|
88
|
+
it("fetches from all 3 exchanges in parallel and compares", async () => {
|
|
89
|
+
setupMockFetch({
|
|
90
|
+
pac: [
|
|
91
|
+
{ symbol: "BTC", funding: 0.0008, mark: 60000 },
|
|
92
|
+
{ symbol: "ETH", funding: 0.0004, mark: 3000 },
|
|
93
|
+
],
|
|
94
|
+
hl: {
|
|
95
|
+
assets: [{ name: "BTC" }, { name: "ETH" }],
|
|
96
|
+
ctxs: [
|
|
97
|
+
{ funding: 0.0002, markPx: 60100 },
|
|
98
|
+
{ funding: 0.0001, markPx: 3010 },
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
lt: {
|
|
102
|
+
details: [
|
|
103
|
+
{ market_id: 1, symbol: "BTC", last_trade_price: 59900 },
|
|
104
|
+
{ market_id: 2, symbol: "ETH", last_trade_price: 2990 },
|
|
105
|
+
],
|
|
106
|
+
funding: [
|
|
107
|
+
{ market_id: 1, rate: 0.0005 },
|
|
108
|
+
{ market_id: 2, rate: 0.0002 },
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
const snapshot = await fetchAllFundingRates();
|
|
113
|
+
expect(snapshot.timestamp).toBeTruthy();
|
|
114
|
+
expect(snapshot.exchangeStatus.pacifica).toBe("ok");
|
|
115
|
+
expect(snapshot.exchangeStatus.hyperliquid).toBe("ok");
|
|
116
|
+
expect(snapshot.exchangeStatus.lighter).toBe("ok");
|
|
117
|
+
expect(snapshot.symbols.length).toBe(2);
|
|
118
|
+
// Should be sorted by spread descending
|
|
119
|
+
const btc = snapshot.symbols.find(s => s.symbol === "BTC");
|
|
120
|
+
expect(btc).toBeTruthy();
|
|
121
|
+
expect(btc.rates.length).toBe(3);
|
|
122
|
+
expect(btc.maxSpreadAnnual).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
it("identifies correct long/short direction", async () => {
|
|
125
|
+
setupMockFetch({
|
|
126
|
+
pac: [{ symbol: "BTC", funding: 0.001, mark: 60000 }],
|
|
127
|
+
hl: {
|
|
128
|
+
assets: [{ name: "BTC" }],
|
|
129
|
+
ctxs: [{ funding: 0.00005, markPx: 60100 }],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
const snapshot = await fetchAllFundingRates();
|
|
133
|
+
const btc = snapshot.symbols.find(s => s.symbol === "BTC");
|
|
134
|
+
expect(btc).toBeTruthy();
|
|
135
|
+
// PAC funding is higher -> short PAC (get paid), long HL (pay less)
|
|
136
|
+
// HL rate per hour = 0.00005, PAC rate per hour = 0.001
|
|
137
|
+
// HL hourly < PAC hourly -> long on HL, short on PAC
|
|
138
|
+
expect(btc.longExchange).toBe("hyperliquid");
|
|
139
|
+
expect(btc.shortExchange).toBe("pacifica");
|
|
140
|
+
});
|
|
141
|
+
it("filters by symbols when specified", async () => {
|
|
142
|
+
setupMockFetch({
|
|
143
|
+
pac: [
|
|
144
|
+
{ symbol: "BTC", funding: 0.0008, mark: 60000 },
|
|
145
|
+
{ symbol: "ETH", funding: 0.0004, mark: 3000 },
|
|
146
|
+
],
|
|
147
|
+
hl: {
|
|
148
|
+
assets: [{ name: "BTC" }, { name: "ETH" }],
|
|
149
|
+
ctxs: [
|
|
150
|
+
{ funding: 0.0001, markPx: 60100 },
|
|
151
|
+
{ funding: 0.00005, markPx: 3010 },
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
const snapshot = await fetchAllFundingRates({ symbols: ["BTC"] });
|
|
156
|
+
expect(snapshot.symbols.length).toBe(1);
|
|
157
|
+
expect(snapshot.symbols[0].symbol).toBe("BTC");
|
|
158
|
+
});
|
|
159
|
+
it("filters by minimum spread", async () => {
|
|
160
|
+
setupMockFetch({
|
|
161
|
+
pac: [
|
|
162
|
+
{ symbol: "BTC", funding: 0.001, mark: 60000 }, // high spread
|
|
163
|
+
{ symbol: "ETH", funding: 0.00011, mark: 3000 }, // tiny spread
|
|
164
|
+
],
|
|
165
|
+
hl: {
|
|
166
|
+
assets: [{ name: "BTC" }, { name: "ETH" }],
|
|
167
|
+
ctxs: [
|
|
168
|
+
{ funding: 0.00005, markPx: 60100 },
|
|
169
|
+
{ funding: 0.0001, markPx: 3010 },
|
|
170
|
+
],
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
const snapshot = await fetchAllFundingRates({ minSpread: 50 });
|
|
174
|
+
// Only BTC should have a spread > 50%
|
|
175
|
+
for (const s of snapshot.symbols) {
|
|
176
|
+
expect(s.maxSpreadAnnual).toBeGreaterThanOrEqual(50);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
it("requires at least 2 exchanges for a symbol", async () => {
|
|
180
|
+
setupMockFetch({
|
|
181
|
+
pac: [{ symbol: "UNIQUE_PAC", funding: 0.001, mark: 100 }],
|
|
182
|
+
hl: { assets: [], ctxs: [] },
|
|
183
|
+
});
|
|
184
|
+
const snapshot = await fetchAllFundingRates();
|
|
185
|
+
// UNIQUE_PAC only on pacifica -> should be excluded
|
|
186
|
+
const unique = snapshot.symbols.find(s => s.symbol === "UNIQUE_PAC");
|
|
187
|
+
expect(unique).toBeUndefined();
|
|
188
|
+
});
|
|
189
|
+
it("handles exchange errors gracefully", async () => {
|
|
190
|
+
setupMockFetch({
|
|
191
|
+
pac: [{ symbol: "BTC", funding: 0.0008, mark: 60000 }],
|
|
192
|
+
hlError: true,
|
|
193
|
+
lt: {
|
|
194
|
+
details: [{ market_id: 1, symbol: "BTC", last_trade_price: 59900 }],
|
|
195
|
+
funding: [{ market_id: 1, rate: 0.0005 }],
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const snapshot = await fetchAllFundingRates();
|
|
199
|
+
expect(snapshot.exchangeStatus.hyperliquid).toBe("error");
|
|
200
|
+
expect(snapshot.exchangeStatus.pacifica).toBe("ok");
|
|
201
|
+
expect(snapshot.exchangeStatus.lighter).toBe("ok");
|
|
202
|
+
// BTC should still be available (2 exchanges: pac + lt)
|
|
203
|
+
const btc = snapshot.symbols.find(s => s.symbol === "BTC");
|
|
204
|
+
expect(btc).toBeTruthy();
|
|
205
|
+
expect(btc.rates.length).toBe(2);
|
|
206
|
+
});
|
|
207
|
+
it("prefers HL mark price as most liquid", async () => {
|
|
208
|
+
setupMockFetch({
|
|
209
|
+
pac: [{ symbol: "BTC", funding: 0.0008, mark: 59000 }],
|
|
210
|
+
hl: {
|
|
211
|
+
assets: [{ name: "BTC" }],
|
|
212
|
+
ctxs: [{ funding: 0.0001, markPx: 60000 }],
|
|
213
|
+
},
|
|
214
|
+
lt: {
|
|
215
|
+
details: [{ market_id: 1, symbol: "BTC", last_trade_price: 59500 }],
|
|
216
|
+
funding: [{ market_id: 1, rate: 0.0005 }],
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
const snapshot = await fetchAllFundingRates();
|
|
220
|
+
const btc = snapshot.symbols.find(s => s.symbol === "BTC");
|
|
221
|
+
expect(btc.bestMarkPrice).toBe(60000); // HL price preferred
|
|
222
|
+
});
|
|
223
|
+
it("estimates positive hourly income for favorable spreads", async () => {
|
|
224
|
+
setupMockFetch({
|
|
225
|
+
pac: [{ symbol: "BTC", funding: 0.002, mark: 60000 }],
|
|
226
|
+
hl: {
|
|
227
|
+
assets: [{ name: "BTC" }],
|
|
228
|
+
ctxs: [{ funding: -0.0001, markPx: 60000 }],
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
const snapshot = await fetchAllFundingRates();
|
|
232
|
+
const btc = snapshot.symbols.find(s => s.symbol === "BTC");
|
|
233
|
+
// Large positive spread -> positive estimated income
|
|
234
|
+
expect(btc.estHourlyIncomeUsd).toBeGreaterThan(0);
|
|
235
|
+
});
|
|
236
|
+
it("returns results sorted by spread descending", async () => {
|
|
237
|
+
setupMockFetch({
|
|
238
|
+
pac: [
|
|
239
|
+
{ symbol: "SMALL", funding: 0.0002, mark: 100 },
|
|
240
|
+
{ symbol: "BIG", funding: 0.003, mark: 200 },
|
|
241
|
+
],
|
|
242
|
+
hl: {
|
|
243
|
+
assets: [{ name: "SMALL" }, { name: "BIG" }],
|
|
244
|
+
ctxs: [
|
|
245
|
+
{ funding: 0.0001, markPx: 100 },
|
|
246
|
+
{ funding: 0.00005, markPx: 200 },
|
|
247
|
+
],
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
const snapshot = await fetchAllFundingRates();
|
|
251
|
+
if (snapshot.symbols.length >= 2) {
|
|
252
|
+
expect(snapshot.symbols[0].maxSpreadAnnual).toBeGreaterThanOrEqual(snapshot.symbols[1].maxSpreadAnnual);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
// ──────────────────────────────────────────────
|
|
257
|
+
// fetchSymbolFundingRates
|
|
258
|
+
// ──────────────────────────────────────────────
|
|
259
|
+
describe("fetchSymbolFundingRates", () => {
|
|
260
|
+
it("returns comparison for a single symbol", async () => {
|
|
261
|
+
setupMockFetch({
|
|
262
|
+
pac: [{ symbol: "ETH", funding: 0.0004, mark: 3000 }],
|
|
263
|
+
hl: {
|
|
264
|
+
assets: [{ name: "ETH" }, { name: "BTC" }],
|
|
265
|
+
ctxs: [
|
|
266
|
+
{ funding: 0.0001, markPx: 3010 },
|
|
267
|
+
{ funding: 0.0002, markPx: 60000 },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const result = await fetchSymbolFundingRates("ETH");
|
|
272
|
+
expect(result).toBeTruthy();
|
|
273
|
+
expect(result.symbol).toBe("ETH");
|
|
274
|
+
expect(result.rates.length).toBe(2);
|
|
275
|
+
});
|
|
276
|
+
it("returns null when symbol not found on 2+ exchanges", async () => {
|
|
277
|
+
setupMockFetch({
|
|
278
|
+
pac: [],
|
|
279
|
+
hl: {
|
|
280
|
+
assets: [{ name: "NOEXIST" }],
|
|
281
|
+
ctxs: [{ funding: 0.0001, markPx: 100 }],
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
const result = await fetchSymbolFundingRates("NOEXIST");
|
|
285
|
+
expect(result).toBeNull();
|
|
286
|
+
});
|
|
287
|
+
it("is case-insensitive", async () => {
|
|
288
|
+
setupMockFetch({
|
|
289
|
+
pac: [{ symbol: "SOL", funding: 0.0005, mark: 150 }],
|
|
290
|
+
hl: {
|
|
291
|
+
assets: [{ name: "SOL" }],
|
|
292
|
+
ctxs: [{ funding: 0.0001, markPx: 151 }],
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
const result = await fetchSymbolFundingRates("sol");
|
|
296
|
+
expect(result).toBeTruthy();
|
|
297
|
+
expect(result.symbol).toBe("SOL");
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
// ──────────────────────────────────────────────
|
|
301
|
+
// 3-DEX direction logic
|
|
302
|
+
// ──────────────────────────────────────────────
|
|
303
|
+
describe("3-DEX direction logic", () => {
|
|
304
|
+
it("picks correct long/short when lighter has best rate", async () => {
|
|
305
|
+
setupMockFetch({
|
|
306
|
+
pac: [{ symbol: "BTC", funding: 0.00006, mark: 60000 }], // hourly = 0.00006
|
|
307
|
+
hl: {
|
|
308
|
+
assets: [{ name: "BTC" }],
|
|
309
|
+
ctxs: [{ funding: 0.0002, markPx: 60100 }], // hourly = 0.0002 (highest)
|
|
310
|
+
},
|
|
311
|
+
lt: {
|
|
312
|
+
details: [{ market_id: 1, symbol: "BTC", last_trade_price: 59900 }],
|
|
313
|
+
funding: [{ market_id: 1, rate: 0.00001 }], // 8h rate, hourly = 0.00001/8 (lowest)
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
const snapshot = await fetchAllFundingRates();
|
|
317
|
+
const btc = snapshot.symbols.find(s => s.symbol === "BTC");
|
|
318
|
+
expect(btc).toBeTruthy();
|
|
319
|
+
// Lighter has lowest hourly rate (0.00001/8) -> long on lighter
|
|
320
|
+
// HL has highest hourly rate (0.0002) -> short on HL
|
|
321
|
+
expect(btc.longExchange).toBe("lighter");
|
|
322
|
+
expect(btc.shortExchange).toBe("hyperliquid");
|
|
323
|
+
});
|
|
324
|
+
it("picks pacifica as short when it has highest rate", async () => {
|
|
325
|
+
setupMockFetch({
|
|
326
|
+
pac: [{ symbol: "ETH", funding: 0.0005, mark: 3000 }], // hourly = 0.0005 (highest)
|
|
327
|
+
hl: {
|
|
328
|
+
assets: [{ name: "ETH" }],
|
|
329
|
+
ctxs: [{ funding: 0.0001, markPx: 3010 }], // hourly = 0.0001 (mid)
|
|
330
|
+
},
|
|
331
|
+
lt: {
|
|
332
|
+
details: [{ market_id: 2, symbol: "ETH", last_trade_price: 2990 }],
|
|
333
|
+
funding: [{ market_id: 2, rate: 0.000025 }], // 8h rate, hourly = 0.000025/8 (lowest)
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
const snapshot = await fetchAllFundingRates();
|
|
337
|
+
const eth = snapshot.symbols.find(s => s.symbol === "ETH");
|
|
338
|
+
expect(eth).toBeTruthy();
|
|
339
|
+
expect(eth.shortExchange).toBe("pacifica");
|
|
340
|
+
expect(eth.longExchange).toBe("lighter");
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getFundingHours, toHourlyRate, annualizeRate, computeAnnualSpread, estimateHourlyFunding, } from "../funding.js";
|
|
3
|
+
// ──────────────────────────────────────────────
|
|
4
|
+
// getFundingHours
|
|
5
|
+
// ──────────────────────────────────────────────
|
|
6
|
+
describe("getFundingHours", () => {
|
|
7
|
+
it("returns 1 for hyperliquid", () => {
|
|
8
|
+
expect(getFundingHours("hyperliquid")).toBe(1);
|
|
9
|
+
});
|
|
10
|
+
it("returns 1 for pacifica", () => {
|
|
11
|
+
expect(getFundingHours("pacifica")).toBe(1);
|
|
12
|
+
});
|
|
13
|
+
it("returns 8 for lighter", () => {
|
|
14
|
+
expect(getFundingHours("lighter")).toBe(8);
|
|
15
|
+
});
|
|
16
|
+
it("defaults to 1 for unknown exchanges (main exchanges are hourly)", () => {
|
|
17
|
+
expect(getFundingHours("binance")).toBe(1);
|
|
18
|
+
expect(getFundingHours("unknown_dex")).toBe(1);
|
|
19
|
+
});
|
|
20
|
+
it("is case-insensitive", () => {
|
|
21
|
+
expect(getFundingHours("Hyperliquid")).toBe(1);
|
|
22
|
+
expect(getFundingHours("PACIFICA")).toBe(1);
|
|
23
|
+
expect(getFundingHours("Lighter")).toBe(8);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
// ──────────────────────────────────────────────
|
|
27
|
+
// toHourlyRate
|
|
28
|
+
// ──────────────────────────────────────────────
|
|
29
|
+
describe("toHourlyRate", () => {
|
|
30
|
+
it("divides by 1 for hyperliquid (rate is already per-hour)", () => {
|
|
31
|
+
const hourly = toHourlyRate(0.0001, "hyperliquid");
|
|
32
|
+
expect(hourly).toBeCloseTo(0.0001);
|
|
33
|
+
});
|
|
34
|
+
it("divides by 1 for pacifica (rate is already per-hour)", () => {
|
|
35
|
+
const hourly = toHourlyRate(0.0001, "pacifica");
|
|
36
|
+
expect(hourly).toBeCloseTo(0.0001);
|
|
37
|
+
});
|
|
38
|
+
it("divides by 8 for lighter (API returns 8h rate)", () => {
|
|
39
|
+
const hourly = toHourlyRate(0.0002, "lighter");
|
|
40
|
+
expect(hourly).toBeCloseTo(0.0002 / 8);
|
|
41
|
+
});
|
|
42
|
+
it("divides by 1 for unknown exchanges (default)", () => {
|
|
43
|
+
const hourly = toHourlyRate(0.0001, "someExchange");
|
|
44
|
+
expect(hourly).toBeCloseTo(0.0001);
|
|
45
|
+
});
|
|
46
|
+
it("handles zero rate", () => {
|
|
47
|
+
expect(toHourlyRate(0, "hyperliquid")).toBe(0);
|
|
48
|
+
expect(toHourlyRate(0, "pacifica")).toBe(0);
|
|
49
|
+
});
|
|
50
|
+
it("handles negative rate", () => {
|
|
51
|
+
const hourly = toHourlyRate(-0.0001, "pacifica");
|
|
52
|
+
expect(hourly).toBeCloseTo(-0.0001);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
// ──────────────────────────────────────────────
|
|
56
|
+
// annualizeRate
|
|
57
|
+
// ──────────────────────────────────────────────
|
|
58
|
+
describe("annualizeRate", () => {
|
|
59
|
+
it("annualizes hyperliquid rate (hourly * 8760 * 100)", () => {
|
|
60
|
+
// rate = 0.0001 per hour → annualized = 0.0001 * 8760 * 100 = 87.6%
|
|
61
|
+
const annual = annualizeRate(0.0001, "hyperliquid");
|
|
62
|
+
expect(annual).toBeCloseTo(87.6);
|
|
63
|
+
});
|
|
64
|
+
it("annualizes pacifica rate (hourly * 8760 * 100)", () => {
|
|
65
|
+
// rate = 0.0001 per hour → annualized = 0.0001 * 8760 * 100 = 87.6%
|
|
66
|
+
const annual = annualizeRate(0.0001, "pacifica");
|
|
67
|
+
expect(annual).toBeCloseTo(87.6);
|
|
68
|
+
});
|
|
69
|
+
it("produces same annualized rate for equivalent rates across exchanges", () => {
|
|
70
|
+
// All exchanges are hourly now, so same rate = same annualized
|
|
71
|
+
const hlAnnual = annualizeRate(0.0001, "hyperliquid");
|
|
72
|
+
const pacAnnual = annualizeRate(0.0001, "pacifica");
|
|
73
|
+
expect(hlAnnual).toBeCloseTo(pacAnnual);
|
|
74
|
+
});
|
|
75
|
+
it("handles zero rate", () => {
|
|
76
|
+
expect(annualizeRate(0, "hyperliquid")).toBe(0);
|
|
77
|
+
expect(annualizeRate(0, "pacifica")).toBe(0);
|
|
78
|
+
});
|
|
79
|
+
it("handles negative rates", () => {
|
|
80
|
+
const annual = annualizeRate(-0.0001, "hyperliquid");
|
|
81
|
+
expect(annual).toBeCloseTo(-87.6);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ──────────────────────────────────────────────
|
|
85
|
+
// computeAnnualSpread
|
|
86
|
+
// ──────────────────────────────────────────────
|
|
87
|
+
describe("computeAnnualSpread", () => {
|
|
88
|
+
it("computes spread between two different exchanges", () => {
|
|
89
|
+
// HL rate 0.0002/h, pacifica rate 0.0001/h → spread = 0.0001/h * 8760 * 100 = 87.6%
|
|
90
|
+
const spread = computeAnnualSpread(0.0002, "hyperliquid", 0.0001, "pacifica");
|
|
91
|
+
expect(spread).toBeCloseTo(87.6);
|
|
92
|
+
});
|
|
93
|
+
it("returns 0 when rates are identical", () => {
|
|
94
|
+
// Both hourly, same rate
|
|
95
|
+
const spread = computeAnnualSpread(0.0001, "hyperliquid", 0.0001, "pacifica");
|
|
96
|
+
expect(spread).toBeCloseTo(0);
|
|
97
|
+
});
|
|
98
|
+
it("returns absolute value regardless of which rate is higher", () => {
|
|
99
|
+
const spread1 = computeAnnualSpread(0.0003, "hyperliquid", 0.0001, "hyperliquid");
|
|
100
|
+
const spread2 = computeAnnualSpread(0.0001, "hyperliquid", 0.0003, "hyperliquid");
|
|
101
|
+
expect(spread1).toBeCloseTo(spread2);
|
|
102
|
+
expect(spread1).toBeGreaterThan(0);
|
|
103
|
+
});
|
|
104
|
+
it("computes spread between pacifica (1h) and lighter (8h)", () => {
|
|
105
|
+
// pacifica raw 0.000125 → hourly = 0.000125
|
|
106
|
+
// lighter raw 0.0000625 → hourly = 0.0000625/8 = 0.0000078125
|
|
107
|
+
// diff: |0.000125 - 0.0000078125| = 0.0001171875/h * 8760 * 100 = 102.66%
|
|
108
|
+
const spread = computeAnnualSpread(0.000125, "pacifica", 0.0000625, "lighter");
|
|
109
|
+
expect(spread).toBeCloseTo(102.66, 1);
|
|
110
|
+
});
|
|
111
|
+
it("handles zero rates", () => {
|
|
112
|
+
const spread = computeAnnualSpread(0, "hyperliquid", 0, "pacifica");
|
|
113
|
+
expect(spread).toBe(0);
|
|
114
|
+
});
|
|
115
|
+
it("handles one zero rate", () => {
|
|
116
|
+
const spread = computeAnnualSpread(0.0001, "hyperliquid", 0, "pacifica");
|
|
117
|
+
// hourly diff = 0.0001, spread = 0.0001 * 8760 * 100 = 87.6%
|
|
118
|
+
expect(spread).toBeCloseTo(87.6);
|
|
119
|
+
});
|
|
120
|
+
it("handles negative rates (one exchange paying, other receiving)", () => {
|
|
121
|
+
// HL pays +0.0001/h, pacifica -0.0001/h
|
|
122
|
+
// diff = |0.0001 - (-0.0001)| = 0.0002/h * 8760 * 100 = 175.2%
|
|
123
|
+
const spread = computeAnnualSpread(0.0001, "hyperliquid", -0.0001, "pacifica");
|
|
124
|
+
expect(spread).toBeCloseTo(175.2);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
// ──────────────────────────────────────────────
|
|
128
|
+
// estimateHourlyFunding
|
|
129
|
+
// ──────────────────────────────────────────────
|
|
130
|
+
describe("estimateHourlyFunding", () => {
|
|
131
|
+
it("long pays positive funding (positive rate)", () => {
|
|
132
|
+
// rate = 0.0001/h (HL), position = $10000
|
|
133
|
+
// hourly payment = 0.0001 * 10000 * 1 = $1
|
|
134
|
+
const payment = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
|
|
135
|
+
expect(payment).toBeCloseTo(1);
|
|
136
|
+
});
|
|
137
|
+
it("short receives positive funding (positive rate)", () => {
|
|
138
|
+
// rate = 0.0001/h (HL), position = $10000
|
|
139
|
+
// hourly payment = 0.0001 * 10000 * (-1) = -$1 (receiving)
|
|
140
|
+
const payment = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "short");
|
|
141
|
+
expect(payment).toBeCloseTo(-1);
|
|
142
|
+
});
|
|
143
|
+
it("long receives negative funding (negative rate)", () => {
|
|
144
|
+
// rate = -0.0001/h, position = $10000
|
|
145
|
+
// hourly = -0.0001 * 10000 * 1 = -$1 (receiving)
|
|
146
|
+
const payment = estimateHourlyFunding(-0.0001, "hyperliquid", 10000, "long");
|
|
147
|
+
expect(payment).toBeCloseTo(-1);
|
|
148
|
+
});
|
|
149
|
+
it("short pays negative funding (negative rate)", () => {
|
|
150
|
+
// rate = -0.0001/h, position = $10000
|
|
151
|
+
// hourly = -0.0001 * 10000 * (-1) = $1 (paying)
|
|
152
|
+
const payment = estimateHourlyFunding(-0.0001, "hyperliquid", 10000, "short");
|
|
153
|
+
expect(payment).toBeCloseTo(1);
|
|
154
|
+
});
|
|
155
|
+
it("uses hourly rate directly for pacifica", () => {
|
|
156
|
+
// rate = 0.0001/h (pacifica), position = $10000
|
|
157
|
+
// long pays: 0.0001 * 10000 = $1
|
|
158
|
+
const payment = estimateHourlyFunding(0.0001, "pacifica", 10000, "long");
|
|
159
|
+
expect(payment).toBeCloseTo(1);
|
|
160
|
+
});
|
|
161
|
+
it("returns 0 for zero funding rate", () => {
|
|
162
|
+
expect(estimateHourlyFunding(0, "hyperliquid", 10000, "long")).toBeCloseTo(0);
|
|
163
|
+
expect(estimateHourlyFunding(0, "pacifica", 10000, "short")).toBeCloseTo(0);
|
|
164
|
+
});
|
|
165
|
+
it("returns 0 for zero position size", () => {
|
|
166
|
+
expect(estimateHourlyFunding(0.0001, "hyperliquid", 0, "long")).toBe(0);
|
|
167
|
+
});
|
|
168
|
+
it("scales linearly with position size", () => {
|
|
169
|
+
const small = estimateHourlyFunding(0.0001, "hyperliquid", 1000, "long");
|
|
170
|
+
const large = estimateHourlyFunding(0.0001, "hyperliquid", 10000, "long");
|
|
171
|
+
expect(large).toBeCloseTo(small * 10);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
function computeGap(symbol, pacPrice, hlPrice) {
|
|
3
|
+
if (pacPrice <= 0 || hlPrice <= 0)
|
|
4
|
+
return null;
|
|
5
|
+
const mid = (pacPrice + hlPrice) / 2;
|
|
6
|
+
const gapPct = ((pacPrice - hlPrice) / mid) * 100;
|
|
7
|
+
return {
|
|
8
|
+
symbol,
|
|
9
|
+
pacPrice,
|
|
10
|
+
hlPrice,
|
|
11
|
+
gapPct,
|
|
12
|
+
direction: pacPrice > hlPrice ? "PAC>HL" : "HL>PAC",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
describe("Price gap computation", () => {
|
|
16
|
+
it("positive gap when PAC > HL", () => {
|
|
17
|
+
const gap = computeGap("BTC", 100100, 99900);
|
|
18
|
+
expect(gap).not.toBeNull();
|
|
19
|
+
expect(gap.gapPct).toBeCloseTo(0.2, 1);
|
|
20
|
+
expect(gap.direction).toBe("PAC>HL");
|
|
21
|
+
});
|
|
22
|
+
it("negative gap when HL > PAC", () => {
|
|
23
|
+
const gap = computeGap("ETH", 3490, 3510);
|
|
24
|
+
expect(gap).not.toBeNull();
|
|
25
|
+
expect(gap.gapPct).toBeLessThan(0);
|
|
26
|
+
expect(gap.direction).toBe("HL>PAC");
|
|
27
|
+
});
|
|
28
|
+
it("zero gap when equal", () => {
|
|
29
|
+
const gap = computeGap("SOL", 150, 150);
|
|
30
|
+
expect(gap).not.toBeNull();
|
|
31
|
+
expect(gap.gapPct).toBe(0);
|
|
32
|
+
});
|
|
33
|
+
it("returns null for zero prices", () => {
|
|
34
|
+
expect(computeGap("X", 0, 100)).toBeNull();
|
|
35
|
+
expect(computeGap("X", 100, 0)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("handles large gaps", () => {
|
|
38
|
+
const gap = computeGap("MEME", 1.0, 0.5);
|
|
39
|
+
expect(gap).not.toBeNull();
|
|
40
|
+
// (1.0 - 0.5) / 0.75 * 100 ≈ 66.67%
|
|
41
|
+
expect(gap.gapPct).toBeCloseTo(66.67, 1);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|