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,86 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { loadPrivateKey, parseSolanaKeypair, isEvmPrivateKey } from "../config.js";
|
|
3
|
+
import { Keypair } from "@solana/web3.js";
|
|
4
|
+
import bs58 from "bs58";
|
|
5
|
+
describe("isEvmPrivateKey", () => {
|
|
6
|
+
it("returns true for valid EVM private key", () => {
|
|
7
|
+
const key = "0x" + "a".repeat(64);
|
|
8
|
+
expect(isEvmPrivateKey(key)).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
it("returns false without 0x prefix", () => {
|
|
11
|
+
expect(isEvmPrivateKey("a".repeat(64))).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
it("returns false for wrong length", () => {
|
|
14
|
+
expect(isEvmPrivateKey("0x" + "a".repeat(32))).toBe(false);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("parseSolanaKeypair", () => {
|
|
18
|
+
it("parses base58-encoded private key", () => {
|
|
19
|
+
const kp = Keypair.generate();
|
|
20
|
+
const b58 = bs58.encode(kp.secretKey);
|
|
21
|
+
const parsed = parseSolanaKeypair(b58);
|
|
22
|
+
expect(parsed.publicKey.toBase58()).toBe(kp.publicKey.toBase58());
|
|
23
|
+
});
|
|
24
|
+
it("parses JSON byte array format", () => {
|
|
25
|
+
const kp = Keypair.generate();
|
|
26
|
+
const jsonArr = JSON.stringify(Array.from(kp.secretKey));
|
|
27
|
+
const parsed = parseSolanaKeypair(jsonArr);
|
|
28
|
+
expect(parsed.publicKey.toBase58()).toBe(kp.publicKey.toBase58());
|
|
29
|
+
});
|
|
30
|
+
it("throws for invalid input", () => {
|
|
31
|
+
expect(() => parseSolanaKeypair("not-a-valid-key")).toThrow("Invalid Solana private key");
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("loadPrivateKey", () => {
|
|
35
|
+
const originalEnv = { ...process.env };
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
// Clear relevant env vars
|
|
38
|
+
delete process.env.PACIFICA_PRIVATE_KEY;
|
|
39
|
+
delete process.env.pk;
|
|
40
|
+
delete process.env.HYPERLIQUID_PRIVATE_KEY;
|
|
41
|
+
delete process.env.HL_PRIVATE_KEY;
|
|
42
|
+
delete process.env.LIGHTER_PRIVATE_KEY;
|
|
43
|
+
delete process.env.PRIVATE_KEY;
|
|
44
|
+
});
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
process.env = { ...originalEnv };
|
|
47
|
+
});
|
|
48
|
+
it("returns pkOverride when provided", async () => {
|
|
49
|
+
const result = await loadPrivateKey("pacifica", "my-override-key");
|
|
50
|
+
expect(result).toBe("my-override-key");
|
|
51
|
+
});
|
|
52
|
+
it("reads exchange-specific env var for pacifica", async () => {
|
|
53
|
+
process.env.PACIFICA_PRIVATE_KEY = "pac-key-123";
|
|
54
|
+
const result = await loadPrivateKey("pacifica");
|
|
55
|
+
expect(result).toBe("pac-key-123");
|
|
56
|
+
});
|
|
57
|
+
it("reads exchange-specific env var for hyperliquid", async () => {
|
|
58
|
+
process.env.HYPERLIQUID_PRIVATE_KEY = "hl-key-456";
|
|
59
|
+
const result = await loadPrivateKey("hyperliquid");
|
|
60
|
+
expect(result).toBe("hl-key-456");
|
|
61
|
+
});
|
|
62
|
+
it("reads HL_PRIVATE_KEY for hyperliquid", async () => {
|
|
63
|
+
process.env.HL_PRIVATE_KEY = "hl-alt-key";
|
|
64
|
+
const result = await loadPrivateKey("hyperliquid");
|
|
65
|
+
expect(result).toBe("hl-alt-key");
|
|
66
|
+
});
|
|
67
|
+
it("reads LIGHTER_PRIVATE_KEY for lighter", async () => {
|
|
68
|
+
process.env.LIGHTER_PRIVATE_KEY = "lighter-key-789";
|
|
69
|
+
const result = await loadPrivateKey("lighter");
|
|
70
|
+
expect(result).toBe("lighter-key-789");
|
|
71
|
+
});
|
|
72
|
+
it("falls back to PRIVATE_KEY", async () => {
|
|
73
|
+
process.env.PRIVATE_KEY = "generic-key";
|
|
74
|
+
const result = await loadPrivateKey("pacifica");
|
|
75
|
+
expect(result).toBe("generic-key");
|
|
76
|
+
});
|
|
77
|
+
it("throws when no key is found", async () => {
|
|
78
|
+
await expect(loadPrivateKey("pacifica")).rejects.toThrow("No private key configured");
|
|
79
|
+
});
|
|
80
|
+
it("prefers exchange-specific over generic", async () => {
|
|
81
|
+
process.env.PACIFICA_PRIVATE_KEY = "specific";
|
|
82
|
+
process.env.PRIVATE_KEY = "generic";
|
|
83
|
+
const result = await loadPrivateKey("pacifica");
|
|
84
|
+
expect(result).toBe("specific");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { checkChainMargins, isCriticalMargin, shouldBlockEntries, computeAutoSize, } from "../cross-chain-margin.js";
|
|
3
|
+
// ── Mock adapter factory ──
|
|
4
|
+
function mockAdapter(opts) {
|
|
5
|
+
const bal = {
|
|
6
|
+
equity: String(opts.equity),
|
|
7
|
+
available: String(opts.available ?? opts.equity - opts.marginUsed),
|
|
8
|
+
marginUsed: String(opts.marginUsed),
|
|
9
|
+
unrealizedPnl: "0",
|
|
10
|
+
};
|
|
11
|
+
return {
|
|
12
|
+
name: opts.name,
|
|
13
|
+
getBalance: vi.fn().mockResolvedValue(bal),
|
|
14
|
+
getOrderbook: vi.fn().mockResolvedValue({
|
|
15
|
+
asks: opts.asks ?? [["100", "10"], ["101", "5"], ["102", "3"]],
|
|
16
|
+
bids: opts.bids ?? [["99", "8"], ["98", "6"], ["97", "4"]],
|
|
17
|
+
}),
|
|
18
|
+
getMarkets: vi.fn(),
|
|
19
|
+
getRecentTrades: vi.fn(),
|
|
20
|
+
getFundingHistory: vi.fn(),
|
|
21
|
+
getKlines: vi.fn(),
|
|
22
|
+
getPositions: vi.fn(),
|
|
23
|
+
getOpenOrders: vi.fn(),
|
|
24
|
+
getOrderHistory: vi.fn(),
|
|
25
|
+
getTradeHistory: vi.fn(),
|
|
26
|
+
getFundingPayments: vi.fn(),
|
|
27
|
+
marketOrder: vi.fn(),
|
|
28
|
+
limitOrder: vi.fn(),
|
|
29
|
+
editOrder: vi.fn(),
|
|
30
|
+
cancelOrder: vi.fn(),
|
|
31
|
+
cancelAllOrders: vi.fn(),
|
|
32
|
+
setLeverage: vi.fn(),
|
|
33
|
+
stopOrder: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
// ── checkChainMargins ──
|
|
37
|
+
describe("checkChainMargins", () => {
|
|
38
|
+
it("returns correct margin status for healthy exchange", async () => {
|
|
39
|
+
const adapters = new Map();
|
|
40
|
+
adapters.set("hyperliquid", mockAdapter({ name: "hyperliquid", equity: 1000, marginUsed: 200 }));
|
|
41
|
+
const statuses = await checkChainMargins(adapters, 30);
|
|
42
|
+
expect(statuses).toHaveLength(1);
|
|
43
|
+
expect(statuses[0].exchange).toBe("hyperliquid");
|
|
44
|
+
expect(statuses[0].chain).toBe("hyperliquid");
|
|
45
|
+
expect(statuses[0].equity).toBe(1000);
|
|
46
|
+
expect(statuses[0].usedMargin).toBe(200);
|
|
47
|
+
expect(statuses[0].freeMargin).toBe(800);
|
|
48
|
+
// marginRatio = (800/1000)*100 = 80%
|
|
49
|
+
expect(statuses[0].marginRatio).toBe(80);
|
|
50
|
+
expect(statuses[0].belowThreshold).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("detects low margin below threshold", async () => {
|
|
53
|
+
const adapters = new Map();
|
|
54
|
+
adapters.set("lighter", mockAdapter({ name: "lighter", equity: 1000, marginUsed: 800 }));
|
|
55
|
+
const statuses = await checkChainMargins(adapters, 30);
|
|
56
|
+
expect(statuses).toHaveLength(1);
|
|
57
|
+
// marginRatio = (200/1000)*100 = 20% < 30%
|
|
58
|
+
expect(statuses[0].marginRatio).toBe(20);
|
|
59
|
+
expect(statuses[0].belowThreshold).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it("handles multiple exchanges", async () => {
|
|
62
|
+
const adapters = new Map();
|
|
63
|
+
adapters.set("hyperliquid", mockAdapter({ name: "hyperliquid", equity: 1000, marginUsed: 100 }));
|
|
64
|
+
adapters.set("pacifica", mockAdapter({ name: "pacifica", equity: 500, marginUsed: 400 }));
|
|
65
|
+
adapters.set("lighter", mockAdapter({ name: "lighter", equity: 2000, marginUsed: 500 }));
|
|
66
|
+
const statuses = await checkChainMargins(adapters, 30);
|
|
67
|
+
expect(statuses).toHaveLength(3);
|
|
68
|
+
const hl = statuses.find(s => s.exchange === "hyperliquid");
|
|
69
|
+
expect(hl.marginRatio).toBe(90); // (900/1000)*100
|
|
70
|
+
expect(hl.belowThreshold).toBe(false);
|
|
71
|
+
expect(hl.chain).toBe("hyperliquid");
|
|
72
|
+
const pac = statuses.find(s => s.exchange === "pacifica");
|
|
73
|
+
expect(pac.marginRatio).toBe(20); // (100/500)*100
|
|
74
|
+
expect(pac.belowThreshold).toBe(true);
|
|
75
|
+
expect(pac.chain).toBe("solana");
|
|
76
|
+
const lt = statuses.find(s => s.exchange === "lighter");
|
|
77
|
+
expect(lt.marginRatio).toBe(75); // (1500/2000)*100
|
|
78
|
+
expect(lt.belowThreshold).toBe(false);
|
|
79
|
+
expect(lt.chain).toBe("arbitrum");
|
|
80
|
+
});
|
|
81
|
+
it("handles zero equity", async () => {
|
|
82
|
+
const adapters = new Map();
|
|
83
|
+
adapters.set("hyperliquid", mockAdapter({ name: "hyperliquid", equity: 0, marginUsed: 0 }));
|
|
84
|
+
const statuses = await checkChainMargins(adapters, 30);
|
|
85
|
+
expect(statuses[0].marginRatio).toBe(0);
|
|
86
|
+
expect(statuses[0].belowThreshold).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
// ── isCriticalMargin ──
|
|
90
|
+
describe("isCriticalMargin", () => {
|
|
91
|
+
it("returns true when margin ratio below 15%", () => {
|
|
92
|
+
const status = {
|
|
93
|
+
exchange: "test",
|
|
94
|
+
chain: "arbitrum",
|
|
95
|
+
equity: 1000,
|
|
96
|
+
usedMargin: 900,
|
|
97
|
+
freeMargin: 100,
|
|
98
|
+
marginRatio: 10,
|
|
99
|
+
belowThreshold: true,
|
|
100
|
+
};
|
|
101
|
+
expect(isCriticalMargin(status)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
it("returns false when margin ratio above 15%", () => {
|
|
104
|
+
const status = {
|
|
105
|
+
exchange: "test",
|
|
106
|
+
chain: "arbitrum",
|
|
107
|
+
equity: 1000,
|
|
108
|
+
usedMargin: 700,
|
|
109
|
+
freeMargin: 300,
|
|
110
|
+
marginRatio: 30,
|
|
111
|
+
belowThreshold: false,
|
|
112
|
+
};
|
|
113
|
+
expect(isCriticalMargin(status)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
it("returns false at exactly 15%", () => {
|
|
116
|
+
const status = {
|
|
117
|
+
exchange: "test",
|
|
118
|
+
chain: "arbitrum",
|
|
119
|
+
equity: 1000,
|
|
120
|
+
usedMargin: 850,
|
|
121
|
+
freeMargin: 150,
|
|
122
|
+
marginRatio: 15,
|
|
123
|
+
belowThreshold: true,
|
|
124
|
+
};
|
|
125
|
+
expect(isCriticalMargin(status)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ── shouldBlockEntries ──
|
|
129
|
+
describe("shouldBlockEntries", () => {
|
|
130
|
+
it("blocks when below threshold", () => {
|
|
131
|
+
const status = {
|
|
132
|
+
exchange: "test",
|
|
133
|
+
chain: "solana",
|
|
134
|
+
equity: 1000,
|
|
135
|
+
usedMargin: 800,
|
|
136
|
+
freeMargin: 200,
|
|
137
|
+
marginRatio: 20,
|
|
138
|
+
belowThreshold: true,
|
|
139
|
+
};
|
|
140
|
+
expect(shouldBlockEntries(status, 30)).toBe(true);
|
|
141
|
+
});
|
|
142
|
+
it("allows when above threshold", () => {
|
|
143
|
+
const status = {
|
|
144
|
+
exchange: "test",
|
|
145
|
+
chain: "solana",
|
|
146
|
+
equity: 1000,
|
|
147
|
+
usedMargin: 500,
|
|
148
|
+
freeMargin: 500,
|
|
149
|
+
marginRatio: 50,
|
|
150
|
+
belowThreshold: false,
|
|
151
|
+
};
|
|
152
|
+
expect(shouldBlockEntries(status, 30)).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
// ── computeAutoSize ──
|
|
156
|
+
describe("computeAutoSize", () => {
|
|
157
|
+
it("picks the smaller side from orderbook depth", async () => {
|
|
158
|
+
// Long side (asks): 3 levels -> $100*10 + $101*5 + $102*3 = $1000+$505+$306 = $1811
|
|
159
|
+
// Short side (bids): 3 levels -> $99*8 + $98*6 + $97*4 = $792+$588+$388 = $1768
|
|
160
|
+
// Min is $1768 (short side)
|
|
161
|
+
const longAdapter = mockAdapter({
|
|
162
|
+
name: "hyperliquid",
|
|
163
|
+
equity: 10000,
|
|
164
|
+
marginUsed: 0,
|
|
165
|
+
asks: [["100", "10"], ["101", "5"], ["102", "3"]],
|
|
166
|
+
});
|
|
167
|
+
const shortAdapter = mockAdapter({
|
|
168
|
+
name: "pacifica",
|
|
169
|
+
equity: 10000,
|
|
170
|
+
marginUsed: 0,
|
|
171
|
+
bids: [["99", "8"], ["98", "6"], ["97", "4"]],
|
|
172
|
+
});
|
|
173
|
+
const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 5.0);
|
|
174
|
+
// Should be capped by risk maxPositionUsd (5000 default) or the orderbook depth
|
|
175
|
+
expect(size).toBeGreaterThan(0);
|
|
176
|
+
expect(size).toBeLessThanOrEqual(5000); // risk limit
|
|
177
|
+
});
|
|
178
|
+
it("respects 50% free margin cap", async () => {
|
|
179
|
+
// Both sides have huge depth but small free margin
|
|
180
|
+
const longAdapter = mockAdapter({
|
|
181
|
+
name: "hyperliquid",
|
|
182
|
+
equity: 200,
|
|
183
|
+
marginUsed: 100,
|
|
184
|
+
asks: [["100", "100"]], // $10000 depth
|
|
185
|
+
});
|
|
186
|
+
const shortAdapter = mockAdapter({
|
|
187
|
+
name: "pacifica",
|
|
188
|
+
equity: 300,
|
|
189
|
+
marginUsed: 100,
|
|
190
|
+
bids: [["99", "100"]], // $9900 depth
|
|
191
|
+
});
|
|
192
|
+
const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 5.0);
|
|
193
|
+
// Free margin: long=100, short=200 -> min=100 -> 50% = 50
|
|
194
|
+
expect(size).toBeLessThanOrEqual(50);
|
|
195
|
+
expect(size).toBeGreaterThan(0);
|
|
196
|
+
});
|
|
197
|
+
it("returns 0 when orderbook is empty", async () => {
|
|
198
|
+
const longAdapter = mockAdapter({
|
|
199
|
+
name: "hyperliquid",
|
|
200
|
+
equity: 10000,
|
|
201
|
+
marginUsed: 0,
|
|
202
|
+
asks: [],
|
|
203
|
+
});
|
|
204
|
+
const shortAdapter = mockAdapter({
|
|
205
|
+
name: "pacifica",
|
|
206
|
+
equity: 10000,
|
|
207
|
+
marginUsed: 0,
|
|
208
|
+
bids: [["99", "10"]],
|
|
209
|
+
});
|
|
210
|
+
const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 0.3);
|
|
211
|
+
expect(size).toBe(0);
|
|
212
|
+
});
|
|
213
|
+
it("caps at maxPositionUsd from risk config", async () => {
|
|
214
|
+
// Huge depth and margin, should be capped by risk limits
|
|
215
|
+
const longAdapter = mockAdapter({
|
|
216
|
+
name: "hyperliquid",
|
|
217
|
+
equity: 1_000_000,
|
|
218
|
+
marginUsed: 0,
|
|
219
|
+
asks: [["100", "10000"]], // $1M depth
|
|
220
|
+
});
|
|
221
|
+
const shortAdapter = mockAdapter({
|
|
222
|
+
name: "pacifica",
|
|
223
|
+
equity: 1_000_000,
|
|
224
|
+
marginUsed: 0,
|
|
225
|
+
bids: [["99", "10000"]], // $990K depth
|
|
226
|
+
});
|
|
227
|
+
const size = await computeAutoSize(longAdapter, shortAdapter, "BTC", 5.0);
|
|
228
|
+
// Default maxPositionUsd is 5000
|
|
229
|
+
expect(size).toBeLessThanOrEqual(5000);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
// ── Rebalance computation ──
|
|
233
|
+
describe("Rebalance computation", () => {
|
|
234
|
+
it("computes correct transfers for 50:50 target", async () => {
|
|
235
|
+
// Import the compute function
|
|
236
|
+
const { computeRebalancePlan } = await import("../rebalance.js");
|
|
237
|
+
const snapshots = [
|
|
238
|
+
{ exchange: "lighter", equity: 800, available: 700, marginUsed: 100, unrealizedPnl: 0 },
|
|
239
|
+
{ exchange: "pacifica", equity: 200, available: 200, marginUsed: 0, unrealizedPnl: 0 },
|
|
240
|
+
];
|
|
241
|
+
const plan = computeRebalancePlan(snapshots, {
|
|
242
|
+
weights: { lighter: 0.5, pacifica: 0.5 },
|
|
243
|
+
minMove: 10,
|
|
244
|
+
reserve: 10,
|
|
245
|
+
});
|
|
246
|
+
// Total available = 900, each should have 450
|
|
247
|
+
// Lighter has 700 (surplus ~250), Pacifica has 200 (deficit ~250)
|
|
248
|
+
expect(plan.moves.length).toBeGreaterThan(0);
|
|
249
|
+
// The move should be from lighter to pacifica
|
|
250
|
+
const move = plan.moves[0];
|
|
251
|
+
expect(move.from).toBe("lighter");
|
|
252
|
+
expect(move.to).toBe("pacifica");
|
|
253
|
+
expect(move.amount).toBeGreaterThan(100); // Should move ~230+ (minus reserve)
|
|
254
|
+
});
|
|
255
|
+
it("returns no moves when already balanced", async () => {
|
|
256
|
+
const { computeRebalancePlan } = await import("../rebalance.js");
|
|
257
|
+
const snapshots = [
|
|
258
|
+
{ exchange: "lighter", equity: 500, available: 500, marginUsed: 0, unrealizedPnl: 0 },
|
|
259
|
+
{ exchange: "pacifica", equity: 500, available: 500, marginUsed: 0, unrealizedPnl: 0 },
|
|
260
|
+
];
|
|
261
|
+
const plan = computeRebalancePlan(snapshots, {
|
|
262
|
+
weights: { lighter: 0.5, pacifica: 0.5 },
|
|
263
|
+
minMove: 10,
|
|
264
|
+
reserve: 10,
|
|
265
|
+
});
|
|
266
|
+
expect(plan.moves.length).toBe(0);
|
|
267
|
+
});
|
|
268
|
+
it("respects 33:33:33 three-way split", async () => {
|
|
269
|
+
const { computeRebalancePlan } = await import("../rebalance.js");
|
|
270
|
+
const snapshots = [
|
|
271
|
+
{ exchange: "lighter", equity: 900, available: 900, marginUsed: 0, unrealizedPnl: 0 },
|
|
272
|
+
{ exchange: "pacifica", equity: 0, available: 0, marginUsed: 0, unrealizedPnl: 0 },
|
|
273
|
+
{ exchange: "hyperliquid", equity: 0, available: 0, marginUsed: 0, unrealizedPnl: 0 },
|
|
274
|
+
];
|
|
275
|
+
const plan = computeRebalancePlan(snapshots, {
|
|
276
|
+
weights: { lighter: 1 / 3, pacifica: 1 / 3, hyperliquid: 1 / 3 },
|
|
277
|
+
minMove: 10,
|
|
278
|
+
reserve: 10,
|
|
279
|
+
});
|
|
280
|
+
// Should move funds from lighter to the other two
|
|
281
|
+
expect(plan.moves.length).toBeGreaterThanOrEqual(1);
|
|
282
|
+
for (const m of plan.moves) {
|
|
283
|
+
expect(m.from).toBe("lighter");
|
|
284
|
+
expect(["pacifica", "hyperliquid"]).toContain(m.to);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { findDexArbPairs } from "../dex-asset-map.js";
|
|
3
|
+
function makeAsset(overrides) {
|
|
4
|
+
return {
|
|
5
|
+
maxLeverage: 10,
|
|
6
|
+
openInterest: 1000,
|
|
7
|
+
volume24h: 50000,
|
|
8
|
+
szDecimals: 3,
|
|
9
|
+
...overrides,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
describe("findDexArbPairs — exact name matching", () => {
|
|
13
|
+
it("finds arb pair for TSLA across xyz and cash dexes", () => {
|
|
14
|
+
const assets = [
|
|
15
|
+
makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392.81, fundingRate: 0.00001 }),
|
|
16
|
+
makeAsset({ raw: "cash:TSLA", base: "TSLA", dex: "cash", markPrice: 392.65, fundingRate: -0.00005 }),
|
|
17
|
+
];
|
|
18
|
+
const pairs = findDexArbPairs(assets);
|
|
19
|
+
expect(pairs).toHaveLength(1);
|
|
20
|
+
expect(pairs[0].underlying).toBe("TSLA");
|
|
21
|
+
expect(pairs[0].long.dex).not.toBe(pairs[0].short.dex);
|
|
22
|
+
expect(pairs[0].annualSpread).toBeGreaterThan(0);
|
|
23
|
+
expect(pairs[0].priceGapPct).toBeLessThan(1);
|
|
24
|
+
});
|
|
25
|
+
it("finds multiple pairs for NVDA across 4 dexes", () => {
|
|
26
|
+
const assets = [
|
|
27
|
+
makeAsset({ raw: "xyz:NVDA", base: "NVDA", dex: "xyz", markPrice: 175.97, fundingRate: 0.00004 }),
|
|
28
|
+
makeAsset({ raw: "flx:NVDA", base: "NVDA", dex: "flx", markPrice: 176.02, fundingRate: 0.0 }),
|
|
29
|
+
makeAsset({ raw: "km:NVDA", base: "NVDA", dex: "km", markPrice: 176.01, fundingRate: -0.00006 }),
|
|
30
|
+
makeAsset({ raw: "cash:NVDA", base: "NVDA", dex: "cash", markPrice: 176.19, fundingRate: -0.000001 }),
|
|
31
|
+
];
|
|
32
|
+
const pairs = findDexArbPairs(assets);
|
|
33
|
+
// 4 dexes → C(4,2) = 6 pairs
|
|
34
|
+
expect(pairs.length).toBe(6);
|
|
35
|
+
// All should be NVDA
|
|
36
|
+
for (const p of pairs) {
|
|
37
|
+
expect(p.underlying).toBe("NVDA");
|
|
38
|
+
}
|
|
39
|
+
// Sorted by spread descending
|
|
40
|
+
for (let i = 1; i < pairs.length; i++) {
|
|
41
|
+
expect(pairs[i].annualSpread).toBeLessThanOrEqual(pairs[i - 1].annualSpread);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
it("includes native HL perps when includeNative=true", () => {
|
|
45
|
+
const assets = [
|
|
46
|
+
makeAsset({ raw: "BTC", base: "BTC", dex: "hl", markPrice: 68000, fundingRate: 0.00001 }),
|
|
47
|
+
makeAsset({ raw: "hyna:BTC", base: "BTC", dex: "hyna", markPrice: 68010, fundingRate: 0.00003 }),
|
|
48
|
+
];
|
|
49
|
+
const withNative = findDexArbPairs(assets, { includeNative: true });
|
|
50
|
+
expect(withNative).toHaveLength(1);
|
|
51
|
+
expect(withNative[0].underlying).toBe("BTC");
|
|
52
|
+
const withoutNative = findDexArbPairs(assets, { includeNative: false });
|
|
53
|
+
expect(withoutNative).toHaveLength(0); // only 1 non-native asset
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("findDexArbPairs — alias matching", () => {
|
|
57
|
+
it("matches CL and OIL as same underlying (CRUDE_OIL_WTI)", () => {
|
|
58
|
+
const assets = [
|
|
59
|
+
makeAsset({ raw: "xyz:CL", base: "CL", dex: "xyz", markPrice: 93.34, fundingRate: -0.0005 }),
|
|
60
|
+
makeAsset({ raw: "flx:OIL", base: "OIL", dex: "flx", markPrice: 93.38, fundingRate: 0.0 }),
|
|
61
|
+
];
|
|
62
|
+
const pairs = findDexArbPairs(assets);
|
|
63
|
+
expect(pairs).toHaveLength(1);
|
|
64
|
+
expect(pairs[0].underlying).toBe("CRUDE_OIL_WTI");
|
|
65
|
+
});
|
|
66
|
+
it("matches kPEPE and 1000PEPE as same underlying", () => {
|
|
67
|
+
const assets = [
|
|
68
|
+
makeAsset({ raw: "kPEPE", base: "kPEPE", dex: "hl", markPrice: 0.015, fundingRate: 0.0001 }),
|
|
69
|
+
makeAsset({ raw: "hyna:1000PEPE", base: "1000PEPE", dex: "hyna", markPrice: 0.0151, fundingRate: 0.0003 }),
|
|
70
|
+
];
|
|
71
|
+
const pairs = findDexArbPairs(assets);
|
|
72
|
+
expect(pairs).toHaveLength(1);
|
|
73
|
+
expect(pairs[0].underlying).toBe("1000PEPE");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("findDexArbPairs — blacklist & price gap filtering", () => {
|
|
77
|
+
it("rejects USAR vs US500 (blacklisted)", () => {
|
|
78
|
+
const assets = [
|
|
79
|
+
makeAsset({ raw: "xyz:USAR", base: "USAR", dex: "xyz", markPrice: 17.63, fundingRate: 0.0003 }),
|
|
80
|
+
makeAsset({ raw: "km:US500", base: "US500", dex: "km", markPrice: 666.69, fundingRate: 0.00001 }),
|
|
81
|
+
];
|
|
82
|
+
// Even without blacklist, price gap (>5%) would filter this out
|
|
83
|
+
const pairs = findDexArbPairs(assets);
|
|
84
|
+
expect(pairs).toHaveLength(0);
|
|
85
|
+
});
|
|
86
|
+
it("rejects SEMI vs SEMIS (blacklisted)", () => {
|
|
87
|
+
const assets = [
|
|
88
|
+
makeAsset({ raw: "km:SEMI", base: "SEMI", dex: "km", markPrice: 319.38, fundingRate: 0.00008 }),
|
|
89
|
+
makeAsset({ raw: "vntl:SEMIS", base: "SEMIS", dex: "vntl", markPrice: 381.21, fundingRate: 0.00001 }),
|
|
90
|
+
];
|
|
91
|
+
const pairs = findDexArbPairs(assets);
|
|
92
|
+
expect(pairs).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
it("rejects pairs with >5% price gap even if same name", () => {
|
|
95
|
+
const assets = [
|
|
96
|
+
makeAsset({ raw: "dexA:FOO", base: "FOO", dex: "dexA", markPrice: 100, fundingRate: 0.001 }),
|
|
97
|
+
makeAsset({ raw: "dexB:FOO", base: "FOO", dex: "dexB", markPrice: 110, fundingRate: -0.001 }),
|
|
98
|
+
];
|
|
99
|
+
// ~9.5% gap → should be filtered
|
|
100
|
+
const pairs = findDexArbPairs(assets, { maxPriceGapPct: 5 });
|
|
101
|
+
expect(pairs).toHaveLength(0);
|
|
102
|
+
// Increase tolerance
|
|
103
|
+
const pairsLoose = findDexArbPairs(assets, { maxPriceGapPct: 15 });
|
|
104
|
+
expect(pairsLoose).toHaveLength(1);
|
|
105
|
+
});
|
|
106
|
+
it("accepts pairs with <5% price gap", () => {
|
|
107
|
+
const assets = [
|
|
108
|
+
makeAsset({ raw: "xyz:GOLD", base: "GOLD", dex: "xyz", markPrice: 5164.60, fundingRate: -0.00001 }),
|
|
109
|
+
makeAsset({ raw: "cash:GOLD", base: "GOLD", dex: "cash", markPrice: 5176.04, fundingRate: -0.00005 }),
|
|
110
|
+
];
|
|
111
|
+
const pairs = findDexArbPairs(assets);
|
|
112
|
+
expect(pairs).toHaveLength(1);
|
|
113
|
+
expect(pairs[0].priceGapPct).toBeLessThan(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("findDexArbPairs — spread calculation", () => {
|
|
117
|
+
it("correctly determines long/short direction", () => {
|
|
118
|
+
const assets = [
|
|
119
|
+
// dexA: high positive funding → expensive to be long
|
|
120
|
+
makeAsset({ raw: "dexA:ETH", base: "ETH", dex: "dexA", markPrice: 1974, fundingRate: 0.001 }),
|
|
121
|
+
// dexB: negative funding → get paid to be long
|
|
122
|
+
makeAsset({ raw: "dexB:ETH", base: "ETH", dex: "dexB", markPrice: 1975, fundingRate: -0.001 }),
|
|
123
|
+
];
|
|
124
|
+
const pairs = findDexArbPairs(assets);
|
|
125
|
+
expect(pairs).toHaveLength(1);
|
|
126
|
+
// Should long on dexB (lower funding) and short on dexA (higher funding)
|
|
127
|
+
expect(pairs[0].long.dex).toBe("dexB");
|
|
128
|
+
expect(pairs[0].short.dex).toBe("dexA");
|
|
129
|
+
});
|
|
130
|
+
it("all dexes use 1h funding period (including HIP-3 deployed)", () => {
|
|
131
|
+
const assets = [
|
|
132
|
+
// Native HL: 1h funding
|
|
133
|
+
makeAsset({ raw: "BTC", base: "BTC", dex: "hl", markPrice: 68000, fundingRate: 0.001 }),
|
|
134
|
+
// Deployed dex: also 1h funding (same rate = no spread)
|
|
135
|
+
makeAsset({ raw: "hyna:BTC", base: "BTC", dex: "hyna", markPrice: 68010, fundingRate: 0.001 }),
|
|
136
|
+
];
|
|
137
|
+
const pairs = findDexArbPairs(assets, { minAnnualSpread: 0 });
|
|
138
|
+
// Same rate on both → spread ≈ 0
|
|
139
|
+
if (pairs.length > 0) {
|
|
140
|
+
expect(pairs[0].annualSpread).toBeLessThan(1);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
it("filters by minAnnualSpread", () => {
|
|
144
|
+
const assets = [
|
|
145
|
+
makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.00001 }),
|
|
146
|
+
makeAsset({ raw: "cash:TSLA", base: "TSLA", dex: "cash", markPrice: 393, fundingRate: 0.00002 }),
|
|
147
|
+
];
|
|
148
|
+
const allPairs = findDexArbPairs(assets, { minAnnualSpread: 0 });
|
|
149
|
+
expect(allPairs.length).toBeGreaterThanOrEqual(1);
|
|
150
|
+
const highOnly = findDexArbPairs(assets, { minAnnualSpread: 999 });
|
|
151
|
+
expect(highOnly).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("findDexArbPairs — same dex exclusion", () => {
|
|
155
|
+
it("does not pair assets from the same dex", () => {
|
|
156
|
+
const assets = [
|
|
157
|
+
makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
|
|
158
|
+
makeAsset({ raw: "xyz:NVDA", base: "NVDA", dex: "xyz", markPrice: 176, fundingRate: -0.001 }),
|
|
159
|
+
];
|
|
160
|
+
const pairs = findDexArbPairs(assets);
|
|
161
|
+
expect(pairs).toHaveLength(0); // different assets, no match
|
|
162
|
+
});
|
|
163
|
+
it("does not pair same asset within same dex", () => {
|
|
164
|
+
// Edge case: shouldn't happen but be safe
|
|
165
|
+
const assets = [
|
|
166
|
+
makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
|
|
167
|
+
makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
|
|
168
|
+
];
|
|
169
|
+
const pairs = findDexArbPairs(assets);
|
|
170
|
+
expect(pairs).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("findDexArbPairs — edge cases", () => {
|
|
174
|
+
it("handles empty assets list", () => {
|
|
175
|
+
expect(findDexArbPairs([])).toHaveLength(0);
|
|
176
|
+
});
|
|
177
|
+
it("handles single asset", () => {
|
|
178
|
+
const assets = [
|
|
179
|
+
makeAsset({ raw: "BTC", base: "BTC", dex: "hl", markPrice: 68000, fundingRate: 0.001 }),
|
|
180
|
+
];
|
|
181
|
+
expect(findDexArbPairs(assets)).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
it("handles all assets from one dex only", () => {
|
|
184
|
+
const assets = [
|
|
185
|
+
makeAsset({ raw: "xyz:TSLA", base: "TSLA", dex: "xyz", markPrice: 392, fundingRate: 0.001 }),
|
|
186
|
+
makeAsset({ raw: "xyz:NVDA", base: "NVDA", dex: "xyz", markPrice: 176, fundingRate: -0.001 }),
|
|
187
|
+
makeAsset({ raw: "xyz:GOLD", base: "GOLD", dex: "xyz", markPrice: 5164, fundingRate: 0.0 }),
|
|
188
|
+
];
|
|
189
|
+
expect(findDexArbPairs(assets)).toHaveLength(0);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|