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,278 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { HyperliquidAdapter } from "../../exchanges/hyperliquid.js";
|
|
3
|
+
import { fetchAllDexAssets, findDexArbPairs, scanDexArb, } from "../../dex-asset-map.js";
|
|
4
|
+
import { startEventStream } from "../../event-stream.js";
|
|
5
|
+
/**
|
|
6
|
+
* Integration tests: validate that ALL API response shapes match what
|
|
7
|
+
* user-facing code expects.
|
|
8
|
+
*
|
|
9
|
+
* Hits real Hyperliquid mainnet API (read-only, no private key needed).
|
|
10
|
+
* Catches shape mismatches between live API data and our TypeScript interfaces.
|
|
11
|
+
*
|
|
12
|
+
* Run:
|
|
13
|
+
* pnpm --filter perp-cli test -- --testPathPattern response-shapes.integration
|
|
14
|
+
*/
|
|
15
|
+
// Dummy private key for read-only operations (never signs anything)
|
|
16
|
+
const DUMMY_KEY = "0x" + "1".repeat(64);
|
|
17
|
+
/**
|
|
18
|
+
* Find a market by base symbol, handling possible suffixes (e.g., "BTC-PERP").
|
|
19
|
+
* The SDK may return symbols with or without a -PERP suffix depending on version.
|
|
20
|
+
*/
|
|
21
|
+
function findMarket(markets, base) {
|
|
22
|
+
return (markets.find((m) => m.symbol === base) ??
|
|
23
|
+
markets.find((m) => m.symbol === `${base}-PERP`) ??
|
|
24
|
+
markets.find((m) => m.symbol.toUpperCase().startsWith(base.toUpperCase())));
|
|
25
|
+
}
|
|
26
|
+
describe("Response Shape Validation (Hyperliquid Mainnet)", () => {
|
|
27
|
+
let adapter;
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
adapter = new HyperliquidAdapter(DUMMY_KEY, false);
|
|
30
|
+
await adapter.init();
|
|
31
|
+
}, 30000);
|
|
32
|
+
// ── 1. ExchangeMarketInfo shape ──────────────────────────────────────
|
|
33
|
+
describe("1. ExchangeMarketInfo shape", () => {
|
|
34
|
+
it("every market has all required fields with correct types", async () => {
|
|
35
|
+
const markets = await adapter.getMarkets();
|
|
36
|
+
expect(markets.length).toBeGreaterThanOrEqual(10);
|
|
37
|
+
// BTC and ETH must be present (may have -PERP suffix)
|
|
38
|
+
const btc = findMarket(markets, "BTC");
|
|
39
|
+
const eth = findMarket(markets, "ETH");
|
|
40
|
+
expect(btc).toBeTruthy();
|
|
41
|
+
expect(eth).toBeTruthy();
|
|
42
|
+
for (const m of markets) {
|
|
43
|
+
// symbol: non-empty string
|
|
44
|
+
expect(typeof m.symbol).toBe("string");
|
|
45
|
+
expect(m.symbol.length).toBeGreaterThan(0);
|
|
46
|
+
// markPrice: string, parseable as number > 0
|
|
47
|
+
expect(typeof m.markPrice).toBe("string");
|
|
48
|
+
const mark = Number(m.markPrice);
|
|
49
|
+
expect(Number.isNaN(mark)).toBe(false);
|
|
50
|
+
expect(mark).toBeGreaterThan(0);
|
|
51
|
+
// indexPrice: string
|
|
52
|
+
expect(typeof m.indexPrice).toBe("string");
|
|
53
|
+
// fundingRate: string, parseable as number
|
|
54
|
+
expect(typeof m.fundingRate).toBe("string");
|
|
55
|
+
const funding = Number(m.fundingRate);
|
|
56
|
+
expect(Number.isNaN(funding)).toBe(false);
|
|
57
|
+
// volume24h: string
|
|
58
|
+
expect(typeof m.volume24h).toBe("string");
|
|
59
|
+
// openInterest: string
|
|
60
|
+
expect(typeof m.openInterest).toBe("string");
|
|
61
|
+
// maxLeverage: number > 0
|
|
62
|
+
expect(typeof m.maxLeverage).toBe("number");
|
|
63
|
+
expect(m.maxLeverage).toBeGreaterThan(0);
|
|
64
|
+
}
|
|
65
|
+
}, 30000);
|
|
66
|
+
});
|
|
67
|
+
// ── 2. Orderbook shape ───────────────────────────────────────────────
|
|
68
|
+
describe("2. ExchangeOrder shape (getOrderbook)", () => {
|
|
69
|
+
it("BTC orderbook has correct bid/ask tuple structure", async () => {
|
|
70
|
+
const book = await adapter.getOrderbook("BTC");
|
|
71
|
+
// bids and asks are arrays
|
|
72
|
+
expect(Array.isArray(book.bids)).toBe(true);
|
|
73
|
+
expect(Array.isArray(book.asks)).toBe(true);
|
|
74
|
+
// Both have entries
|
|
75
|
+
expect(book.bids.length).toBeGreaterThan(0);
|
|
76
|
+
expect(book.asks.length).toBeGreaterThan(0);
|
|
77
|
+
// Each entry is a [price, size] tuple of strings
|
|
78
|
+
for (const [price, size] of book.bids) {
|
|
79
|
+
expect(typeof price).toBe("string");
|
|
80
|
+
expect(typeof size).toBe("string");
|
|
81
|
+
expect(Number.isNaN(Number(price))).toBe(false);
|
|
82
|
+
expect(Number.isNaN(Number(size))).toBe(false);
|
|
83
|
+
}
|
|
84
|
+
for (const [price, size] of book.asks) {
|
|
85
|
+
expect(typeof price).toBe("string");
|
|
86
|
+
expect(typeof size).toBe("string");
|
|
87
|
+
expect(Number.isNaN(Number(price))).toBe(false);
|
|
88
|
+
expect(Number.isNaN(Number(size))).toBe(false);
|
|
89
|
+
}
|
|
90
|
+
// Spread is positive: best bid < best ask
|
|
91
|
+
const bestBid = Number(book.bids[0][0]);
|
|
92
|
+
const bestAsk = Number(book.asks[0][0]);
|
|
93
|
+
expect(bestBid).toBeLessThan(bestAsk);
|
|
94
|
+
}, 30000);
|
|
95
|
+
});
|
|
96
|
+
// ── 3. HIP-3 Deployed Dexes shape ───────────────────────────────────
|
|
97
|
+
describe("3. HIP-3 Deployed Dexes shape", () => {
|
|
98
|
+
it("listDeployedDexes returns correctly shaped dex entries", async () => {
|
|
99
|
+
const dexes = await adapter.listDeployedDexes();
|
|
100
|
+
expect(Array.isArray(dexes)).toBe(true);
|
|
101
|
+
expect(dexes.length).toBeGreaterThanOrEqual(3);
|
|
102
|
+
for (const dex of dexes) {
|
|
103
|
+
// name: non-empty string
|
|
104
|
+
expect(typeof dex.name).toBe("string");
|
|
105
|
+
expect(dex.name.length).toBeGreaterThan(0);
|
|
106
|
+
// assets: string array with length > 0
|
|
107
|
+
expect(Array.isArray(dex.assets)).toBe(true);
|
|
108
|
+
expect(dex.assets.length).toBeGreaterThan(0);
|
|
109
|
+
for (const asset of dex.assets) {
|
|
110
|
+
expect(typeof asset).toBe("string");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Known dex "xyz" is present
|
|
114
|
+
const xyz = dexes.find((d) => d.name === "xyz");
|
|
115
|
+
expect(xyz).toBeTruthy();
|
|
116
|
+
// Asset names from deployed dexes contain ":" prefix (e.g., "xyz:TSLA")
|
|
117
|
+
for (const dex of dexes) {
|
|
118
|
+
for (const asset of dex.assets) {
|
|
119
|
+
expect(asset).toContain(":");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}, 30000);
|
|
123
|
+
});
|
|
124
|
+
// ── 4. DexAsset shape from fetchAllDexAssets ─────────────────────────
|
|
125
|
+
describe("4. DexAsset shape from fetchAllDexAssets", () => {
|
|
126
|
+
let allAssets;
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
allAssets = await fetchAllDexAssets();
|
|
129
|
+
}, 30000);
|
|
130
|
+
it("every asset has all required fields with correct types", () => {
|
|
131
|
+
expect(allAssets.length).toBeGreaterThan(200);
|
|
132
|
+
for (const asset of allAssets) {
|
|
133
|
+
// raw: non-empty string
|
|
134
|
+
expect(typeof asset.raw).toBe("string");
|
|
135
|
+
expect(asset.raw.length).toBeGreaterThan(0);
|
|
136
|
+
// base: non-empty string
|
|
137
|
+
expect(typeof asset.base).toBe("string");
|
|
138
|
+
expect(asset.base.length).toBeGreaterThan(0);
|
|
139
|
+
// dex: non-empty string
|
|
140
|
+
expect(typeof asset.dex).toBe("string");
|
|
141
|
+
expect(asset.dex.length).toBeGreaterThan(0);
|
|
142
|
+
// markPrice: number > 0
|
|
143
|
+
expect(typeof asset.markPrice).toBe("number");
|
|
144
|
+
expect(asset.markPrice).toBeGreaterThan(0);
|
|
145
|
+
// fundingRate: number (not NaN)
|
|
146
|
+
expect(typeof asset.fundingRate).toBe("number");
|
|
147
|
+
expect(Number.isNaN(asset.fundingRate)).toBe(false);
|
|
148
|
+
// maxLeverage: number
|
|
149
|
+
expect(typeof asset.maxLeverage).toBe("number");
|
|
150
|
+
// openInterest: number
|
|
151
|
+
expect(typeof asset.openInterest).toBe("number");
|
|
152
|
+
// volume24h: number
|
|
153
|
+
expect(typeof asset.volume24h).toBe("number");
|
|
154
|
+
// szDecimals: number >= 0
|
|
155
|
+
expect(typeof asset.szDecimals).toBe("number");
|
|
156
|
+
expect(asset.szDecimals).toBeGreaterThanOrEqual(0);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
it("assets come from at least 4 different dexes", () => {
|
|
160
|
+
const dexes = new Set(allAssets.map((a) => a.dex));
|
|
161
|
+
expect(dexes.size).toBeGreaterThanOrEqual(4);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
// ── 5. DexArbPair shape from findDexArbPairs ────────────────────────
|
|
165
|
+
describe("5. DexArbPair shape from findDexArbPairs", () => {
|
|
166
|
+
it("TSLA pairs across dexes have correct shape and constraints", async () => {
|
|
167
|
+
const allAssets = await fetchAllDexAssets();
|
|
168
|
+
const tslaAssets = allAssets.filter((a) => a.base === "TSLA");
|
|
169
|
+
// TSLA should exist on multiple dexes
|
|
170
|
+
expect(tslaAssets.length).toBeGreaterThanOrEqual(2);
|
|
171
|
+
const pairs = findDexArbPairs(tslaAssets);
|
|
172
|
+
expect(pairs.length).toBeGreaterThanOrEqual(1);
|
|
173
|
+
for (const pair of pairs) {
|
|
174
|
+
// underlying: string
|
|
175
|
+
expect(typeof pair.underlying).toBe("string");
|
|
176
|
+
expect(pair.underlying).toBe("TSLA");
|
|
177
|
+
// long and short: DexAsset objects
|
|
178
|
+
expect(pair.long).toBeTruthy();
|
|
179
|
+
expect(pair.short).toBeTruthy();
|
|
180
|
+
expect(typeof pair.long.dex).toBe("string");
|
|
181
|
+
expect(typeof pair.short.dex).toBe("string");
|
|
182
|
+
// long.dex !== short.dex
|
|
183
|
+
expect(pair.long.dex).not.toBe(pair.short.dex);
|
|
184
|
+
// priceGapPct < 5 (same underlying, prices should be close)
|
|
185
|
+
expect(pair.priceGapPct).toBeLessThan(5);
|
|
186
|
+
// annualSpread: reasonable number (not NaN, not Infinity)
|
|
187
|
+
expect(typeof pair.annualSpread).toBe("number");
|
|
188
|
+
expect(Number.isNaN(pair.annualSpread)).toBe(false);
|
|
189
|
+
expect(Number.isFinite(pair.annualSpread)).toBe(true);
|
|
190
|
+
}
|
|
191
|
+
}, 30000);
|
|
192
|
+
});
|
|
193
|
+
// ── 6. scanDexArb full pipeline ──────────────────────────────────────
|
|
194
|
+
describe("6. scanDexArb full pipeline", () => {
|
|
195
|
+
it("returns sorted array with valid pairs", async () => {
|
|
196
|
+
const pairs = await scanDexArb({ minAnnualSpread: 5 });
|
|
197
|
+
expect(Array.isArray(pairs)).toBe(true);
|
|
198
|
+
// Sorted by annualSpread descending
|
|
199
|
+
for (let i = 1; i < pairs.length; i++) {
|
|
200
|
+
expect(pairs[i].annualSpread).toBeLessThanOrEqual(pairs[i - 1].annualSpread);
|
|
201
|
+
}
|
|
202
|
+
// Every pair has all required fields and no same-dex pairs
|
|
203
|
+
for (const pair of pairs) {
|
|
204
|
+
expect(typeof pair.underlying).toBe("string");
|
|
205
|
+
expect(pair.underlying.length).toBeGreaterThan(0);
|
|
206
|
+
expect(pair.long).toBeTruthy();
|
|
207
|
+
expect(pair.short).toBeTruthy();
|
|
208
|
+
expect(pair.long.dex).not.toBe(pair.short.dex);
|
|
209
|
+
expect(typeof pair.annualSpread).toBe("number");
|
|
210
|
+
expect(pair.annualSpread).toBeGreaterThanOrEqual(5);
|
|
211
|
+
expect(Number.isFinite(pair.annualSpread)).toBe(true);
|
|
212
|
+
expect(typeof pair.priceGapPct).toBe("number");
|
|
213
|
+
expect(Number.isFinite(pair.priceGapPct)).toBe(true);
|
|
214
|
+
}
|
|
215
|
+
}, 30000);
|
|
216
|
+
});
|
|
217
|
+
// ── 7. Event stream shape (single poll cycle) ───────────────────────
|
|
218
|
+
describe("7. Event stream shape (single poll cycle)", () => {
|
|
219
|
+
it("emits correctly shaped events without crashing", async () => {
|
|
220
|
+
const events = [];
|
|
221
|
+
const controller = new AbortController();
|
|
222
|
+
// Run a single poll cycle then abort
|
|
223
|
+
const streamPromise = startEventStream(adapter, {
|
|
224
|
+
intervalMs: 100_000, // large interval so we only get one poll
|
|
225
|
+
onEvent: (event) => {
|
|
226
|
+
events.push(event);
|
|
227
|
+
},
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
});
|
|
230
|
+
// Wait briefly for the initial poll to complete, then abort
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
232
|
+
controller.abort();
|
|
233
|
+
// Wait for the stream to finish
|
|
234
|
+
await streamPromise;
|
|
235
|
+
// With a dummy key, positions/orders may be empty but the poll should
|
|
236
|
+
// complete without crashing. Any emitted events must conform to shape.
|
|
237
|
+
for (const event of events) {
|
|
238
|
+
// type: valid EventType string
|
|
239
|
+
expect(typeof event.type).toBe("string");
|
|
240
|
+
expect(event.type.length).toBeGreaterThan(0);
|
|
241
|
+
// exchange: string
|
|
242
|
+
expect(typeof event.exchange).toBe("string");
|
|
243
|
+
expect(event.exchange).toBe("hyperliquid");
|
|
244
|
+
// timestamp: ISO format string
|
|
245
|
+
expect(typeof event.timestamp).toBe("string");
|
|
246
|
+
expect(new Date(event.timestamp).toISOString()).toBe(event.timestamp);
|
|
247
|
+
// data: object
|
|
248
|
+
expect(typeof event.data).toBe("object");
|
|
249
|
+
expect(event.data).not.toBeNull();
|
|
250
|
+
}
|
|
251
|
+
}, 30000);
|
|
252
|
+
});
|
|
253
|
+
// ── 8. Funding rate / price sanity checks ────────────────────────────
|
|
254
|
+
describe("8. Funding rate and price sanity checks", () => {
|
|
255
|
+
it("BTC and ETH have reasonable funding rates and prices", async () => {
|
|
256
|
+
const markets = await adapter.getMarkets();
|
|
257
|
+
// Handle possible -PERP suffix from SDK
|
|
258
|
+
const btc = findMarket(markets, "BTC");
|
|
259
|
+
const eth = findMarket(markets, "ETH");
|
|
260
|
+
expect(btc).toBeTruthy();
|
|
261
|
+
expect(eth).toBeTruthy();
|
|
262
|
+
// BTC funding rate between -0.01 and 0.01 (1% per period is extreme)
|
|
263
|
+
const btcFunding = Number(btc.fundingRate);
|
|
264
|
+
expect(btcFunding).toBeGreaterThan(-0.01);
|
|
265
|
+
expect(btcFunding).toBeLessThan(0.01);
|
|
266
|
+
// ETH funding rate between -0.01 and 0.01
|
|
267
|
+
const ethFunding = Number(eth.fundingRate);
|
|
268
|
+
expect(ethFunding).toBeGreaterThan(-0.01);
|
|
269
|
+
expect(ethFunding).toBeLessThan(0.01);
|
|
270
|
+
// BTC mark price > $100 (catches off-by-order-of-magnitude parsing bugs)
|
|
271
|
+
const btcPrice = Number(btc.markPrice);
|
|
272
|
+
expect(btcPrice).toBeGreaterThan(100);
|
|
273
|
+
// ETH mark price > $10
|
|
274
|
+
const ethPrice = Number(eth.markPrice);
|
|
275
|
+
expect(ethPrice).toBeGreaterThan(10);
|
|
276
|
+
}, 30000);
|
|
277
|
+
});
|
|
278
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { computeExecutableSize, checkArbLiquidity } from "../liquidity.js";
|
|
3
|
+
// ──────────────────────────────────────────────
|
|
4
|
+
// computeExecutableSize — orderbook walking
|
|
5
|
+
// ──────────────────────────────────────────────
|
|
6
|
+
describe("computeExecutableSize — empty book", () => {
|
|
7
|
+
it("returns zeroed result for empty levels", () => {
|
|
8
|
+
const result = computeExecutableSize([], 1000);
|
|
9
|
+
expect(result.maxSize).toBe(0);
|
|
10
|
+
expect(result.avgFillPrice).toBe(0);
|
|
11
|
+
expect(result.slippagePct).toBe(0);
|
|
12
|
+
expect(result.depthUsd).toBe(0);
|
|
13
|
+
expect(result.canFillFull).toBe(false);
|
|
14
|
+
expect(result.recommendedSize).toBe(0);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe("computeExecutableSize — single level", () => {
|
|
18
|
+
it("fills entirely from one level when sufficient", () => {
|
|
19
|
+
// One level at $100, size 100 = $10,000 USD. Request $500.
|
|
20
|
+
const result = computeExecutableSize([["100", "100"]], 500);
|
|
21
|
+
expect(result.maxSize).toBeCloseTo(5); // $500 / $100
|
|
22
|
+
expect(result.avgFillPrice).toBeCloseTo(100);
|
|
23
|
+
expect(result.slippagePct).toBeCloseTo(0);
|
|
24
|
+
expect(result.canFillFull).toBe(true);
|
|
25
|
+
expect(result.depthUsd).toBeCloseTo(10000);
|
|
26
|
+
});
|
|
27
|
+
it("partially fills when level is insufficient", () => {
|
|
28
|
+
// One level at $100, size 2 = $200 USD. Request $500.
|
|
29
|
+
const result = computeExecutableSize([["100", "2"]], 500);
|
|
30
|
+
expect(result.maxSize).toBeCloseTo(2);
|
|
31
|
+
expect(result.avgFillPrice).toBeCloseTo(100);
|
|
32
|
+
// Can't fill 95% → canFillFull = false
|
|
33
|
+
expect(result.canFillFull).toBe(false);
|
|
34
|
+
expect(result.depthUsd).toBeCloseTo(200);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe("computeExecutableSize — multiple levels", () => {
|
|
38
|
+
it("walks through levels to fill requested size", () => {
|
|
39
|
+
// Use prices well within slippage tolerance (default 0.5%)
|
|
40
|
+
// bestPrice = 100, slippageLimit = 100.5
|
|
41
|
+
// All prices must be <= 100.5 to not be stopped by slippage check
|
|
42
|
+
const levels = [
|
|
43
|
+
["100", "5"], // $500
|
|
44
|
+
["100.2", "5"], // $501 — well within 0.5% of $100
|
|
45
|
+
["100.4", "10"], // $1004 — also within 0.5%
|
|
46
|
+
];
|
|
47
|
+
// Request $1000. Level 1: $500 fully consumed (5 units).
|
|
48
|
+
// Level 2: remaining $500, levelUsd $501 > $500, partial fill: 500/100.2 ≈ 4.99 units.
|
|
49
|
+
const result = computeExecutableSize(levels, 1000);
|
|
50
|
+
expect(result.maxSize).toBeCloseTo(5 + 500 / 100.2, 2);
|
|
51
|
+
expect(result.canFillFull).toBe(true);
|
|
52
|
+
expect(result.avgFillPrice).toBeCloseTo(1000 / result.maxSize, 2);
|
|
53
|
+
expect(result.slippagePct).toBeGreaterThan(0);
|
|
54
|
+
expect(result.slippagePct).toBeLessThan(0.5);
|
|
55
|
+
});
|
|
56
|
+
it("computes slippage as avg fill vs best price", () => {
|
|
57
|
+
// Use wider slippage tolerance (5%) so all levels are included
|
|
58
|
+
const levels = [
|
|
59
|
+
["1000", "0.1"], // $100
|
|
60
|
+
["1010", "0.1"], // $101
|
|
61
|
+
["1020", "10"], // $10,200
|
|
62
|
+
];
|
|
63
|
+
// With 5% tolerance, slippageLimit = 1000 * 1.05 = 1050
|
|
64
|
+
// All levels are within tolerance.
|
|
65
|
+
// Request $10,000. Level 1: $100 (fully consumed). Level 2: $101 (fully consumed).
|
|
66
|
+
// Level 3: remaining = $9799, levelUsd = $10200 > remaining → partial fill.
|
|
67
|
+
const result = computeExecutableSize(levels, 10000, 5);
|
|
68
|
+
// Most of the fill happens at $1020
|
|
69
|
+
expect(result.avgFillPrice).toBeGreaterThan(1000);
|
|
70
|
+
// slippagePct = abs((avgFillPrice - 1000) / 1000) * 100
|
|
71
|
+
expect(result.slippagePct).toBeGreaterThan(1);
|
|
72
|
+
expect(result.slippagePct).toBeLessThan(3);
|
|
73
|
+
});
|
|
74
|
+
it("stops walking when slippage limit is exceeded", () => {
|
|
75
|
+
const levels = [
|
|
76
|
+
["100", "1"], // $100
|
|
77
|
+
["100.4", "1"], // $100.40 — within 0.5%
|
|
78
|
+
["101", "100"], // $10,100 — 1% above best, exceeds 0.5% default
|
|
79
|
+
];
|
|
80
|
+
// Request $5000 with default 0.5% slippage.
|
|
81
|
+
// slippageLimit = 100 * (1 + 0.5/100) = 100.5
|
|
82
|
+
// Level 1: $100 (consumed), level 2: $100.4 (consumed), level 3: $101 > $100.5 → stop
|
|
83
|
+
const result = computeExecutableSize(levels, 5000, 0.5);
|
|
84
|
+
// Should only consume levels 1 and 2
|
|
85
|
+
expect(result.maxSize).toBeCloseTo(2);
|
|
86
|
+
expect(result.canFillFull).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
it("respects custom slippage tolerance", () => {
|
|
89
|
+
const levels = [
|
|
90
|
+
["100", "1"],
|
|
91
|
+
["102", "100"], // 2% above best
|
|
92
|
+
];
|
|
93
|
+
// With 3% tolerance, level 2 is within range
|
|
94
|
+
const result = computeExecutableSize(levels, 5000, 3);
|
|
95
|
+
expect(result.canFillFull).toBe(true);
|
|
96
|
+
expect(result.maxSize).toBeGreaterThan(1);
|
|
97
|
+
});
|
|
98
|
+
it("considers canFillFull at 95% threshold", () => {
|
|
99
|
+
// Need $1000. If we fill $950, canFillFull = true (95%)
|
|
100
|
+
const levels = [
|
|
101
|
+
["100", "9.5"], // $950
|
|
102
|
+
];
|
|
103
|
+
const result = computeExecutableSize(levels, 1000);
|
|
104
|
+
expect(result.canFillFull).toBe(true);
|
|
105
|
+
// Need $1000. If we fill $940, canFillFull = false (94%)
|
|
106
|
+
const levels2 = [
|
|
107
|
+
["100", "9.4"], // $940
|
|
108
|
+
];
|
|
109
|
+
const result2 = computeExecutableSize(levels2, 1000);
|
|
110
|
+
expect(result2.canFillFull).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe("computeExecutableSize — depth calculation", () => {
|
|
114
|
+
it("accumulates total depth across all iterated levels", () => {
|
|
115
|
+
const levels = [
|
|
116
|
+
["100", "10"], // $1000
|
|
117
|
+
["101", "10"], // $1010
|
|
118
|
+
["102", "10"], // $1020
|
|
119
|
+
];
|
|
120
|
+
// Request $500. Level 1 has $1000 > $500, so partial fill on level 1.
|
|
121
|
+
// But the loop: totalDepthUsd is accumulated BEFORE checking remainingUsd.
|
|
122
|
+
// So level 1: totalDepthUsd += $1000, then fills $500 partial, then remainingUsd = 0.
|
|
123
|
+
// Level 2: totalDepthUsd += $1010 (accumulated before the break on remainingUsd <= 0).
|
|
124
|
+
// Actually: after level 1 fill, filledNotional = $500, so remainingUsd = 0 → breaks at start of level 2.
|
|
125
|
+
// Wait — let's trace: for level 1, totalDepthUsd += 1000, price 100 not > slippageLimit,
|
|
126
|
+
// remaining = 500, levelUsd = 1000 > 500 → partial fill. filledNotional = 500.
|
|
127
|
+
// Next iteration level 2: totalDepthUsd += 1010, then remaining = 500 - 500 = 0 → break.
|
|
128
|
+
// So totalDepthUsd = 1000 + 1010 = 2010.
|
|
129
|
+
const result = computeExecutableSize(levels, 500);
|
|
130
|
+
expect(result.depthUsd).toBeCloseTo(2010);
|
|
131
|
+
// But only $500 was actually filled from level 1
|
|
132
|
+
expect(result.maxSize).toBeCloseTo(5); // 500 / 100
|
|
133
|
+
expect(result.canFillFull).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
// ──────────────────────────────────────────────
|
|
137
|
+
// checkArbLiquidity — cross-exchange check
|
|
138
|
+
// ──────────────────────────────────────────────
|
|
139
|
+
function mockAdapter(name, asks, bids) {
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
getOrderbook: vi.fn().mockResolvedValue({ asks, bids }),
|
|
143
|
+
getMarkets: vi.fn().mockResolvedValue([]),
|
|
144
|
+
getBalance: vi.fn().mockResolvedValue({ equity: "1000", available: "800", marginUsed: "200", unrealizedPnl: "0" }),
|
|
145
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
146
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
147
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
148
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
149
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
150
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
151
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
152
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
153
|
+
marketOrder: vi.fn().mockResolvedValue({}),
|
|
154
|
+
limitOrder: vi.fn().mockResolvedValue({}),
|
|
155
|
+
editOrder: vi.fn().mockResolvedValue({}),
|
|
156
|
+
cancelOrder: vi.fn().mockResolvedValue({}),
|
|
157
|
+
cancelAllOrders: vi.fn().mockResolvedValue({}),
|
|
158
|
+
setLeverage: vi.fn().mockResolvedValue({}),
|
|
159
|
+
stopOrder: vi.fn().mockResolvedValue({}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
describe("checkArbLiquidity", () => {
|
|
163
|
+
it("returns viable when both sides have sufficient liquidity", async () => {
|
|
164
|
+
const longAdapter = mockAdapter("exchange-a", [["60000", "10"], ["60010", "10"]], // asks
|
|
165
|
+
[["59990", "10"]]);
|
|
166
|
+
const shortAdapter = mockAdapter("exchange-b", [["60005", "10"]], // asks
|
|
167
|
+
[["59995", "10"], ["59985", "10"]]);
|
|
168
|
+
const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
|
|
169
|
+
expect(result.viable).toBe(true);
|
|
170
|
+
expect(result.adjustedSizeUsd).toBeGreaterThan(0);
|
|
171
|
+
expect(result.adjustedSizeUsd).toBeLessThanOrEqual(1000);
|
|
172
|
+
});
|
|
173
|
+
it("returns not viable when liquidity is too thin (less than 20% of requested)", async () => {
|
|
174
|
+
const longAdapter = mockAdapter("exchange-a", [["60000", "0.001"]], // asks: ~$60
|
|
175
|
+
[["59990", "1"]]);
|
|
176
|
+
const shortAdapter = mockAdapter("exchange-b", [["60005", "1"]], [["59995", "0.001"]]);
|
|
177
|
+
// Request $10000. Both sides have ~$60 → way below 20% = $2000 threshold
|
|
178
|
+
const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 10000);
|
|
179
|
+
expect(result.viable).toBe(false);
|
|
180
|
+
expect(result.adjustedSizeUsd).toBe(0);
|
|
181
|
+
});
|
|
182
|
+
it("returns not viable when cross-exchange price gap exceeds 2%", async () => {
|
|
183
|
+
const longAdapter = mockAdapter("exchange-a", [["60000", "100"]], // asks
|
|
184
|
+
[["59900", "100"]]);
|
|
185
|
+
const shortAdapter = mockAdapter("exchange-b", [["62000", "100"]], // asks
|
|
186
|
+
[["61500", "100"]]);
|
|
187
|
+
const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
|
|
188
|
+
expect(result.viable).toBe(false);
|
|
189
|
+
});
|
|
190
|
+
it("adjusts size down when one side has limited liquidity", async () => {
|
|
191
|
+
const longAdapter = mockAdapter("exchange-a", [["60000", "0.05"]], // asks: $3000
|
|
192
|
+
[["59990", "10"]]);
|
|
193
|
+
const shortAdapter = mockAdapter("exchange-b", [["60010", "10"]], [["59995", "10"]]);
|
|
194
|
+
// Request $5000. Long side only has $3000 on asks.
|
|
195
|
+
const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 5000);
|
|
196
|
+
expect(result.viable).toBe(true);
|
|
197
|
+
expect(result.adjustedSizeUsd).toBeLessThan(5000);
|
|
198
|
+
expect(result.adjustedSizeUsd).toBeGreaterThan(0);
|
|
199
|
+
});
|
|
200
|
+
it("returns not viable when orderbook fetch fails", async () => {
|
|
201
|
+
const longAdapter = mockAdapter("exchange-a", [], []);
|
|
202
|
+
longAdapter.getOrderbook = vi.fn().mockRejectedValue(new Error("timeout"));
|
|
203
|
+
const shortAdapter = mockAdapter("exchange-b", [], []);
|
|
204
|
+
const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
|
|
205
|
+
expect(result.viable).toBe(false);
|
|
206
|
+
expect(result.adjustedSizeUsd).toBe(0);
|
|
207
|
+
});
|
|
208
|
+
it("invokes log callback for diagnostics", async () => {
|
|
209
|
+
const longAdapter = mockAdapter("exchange-a", [["60000", "0.001"]], // tiny asks
|
|
210
|
+
[["59990", "1"]]);
|
|
211
|
+
const shortAdapter = mockAdapter("exchange-b", [["60005", "1"]], [["59995", "0.001"]]);
|
|
212
|
+
const logFn = vi.fn();
|
|
213
|
+
await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 10000, 0.5, logFn);
|
|
214
|
+
expect(logFn).toHaveBeenCalled();
|
|
215
|
+
expect(logFn.mock.calls[0][0]).toContain("[LIQ]");
|
|
216
|
+
});
|
|
217
|
+
it("caps adjusted size to requested size when liquidity is ample", async () => {
|
|
218
|
+
const longAdapter = mockAdapter("exchange-a", [["60000", "100"]], // $6M asks
|
|
219
|
+
[["59990", "100"]]);
|
|
220
|
+
const shortAdapter = mockAdapter("exchange-b", [["60005", "100"]], [["59995", "100"]]);
|
|
221
|
+
const result = await checkArbLiquidity(longAdapter, shortAdapter, "BTC", 1000);
|
|
222
|
+
expect(result.viable).toBe(true);
|
|
223
|
+
expect(result.adjustedSizeUsd).toBeLessThanOrEqual(1000);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|