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,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E integration tests for new atomic commands and api-spec.
|
|
3
|
+
* These tests spawn the actual CLI process and verify JSON output.
|
|
4
|
+
*
|
|
5
|
+
* - api-spec: no adapter needed, always works
|
|
6
|
+
* - market mid: needs HL mainnet (read-only, no key needed for public data)
|
|
7
|
+
* - error envelopes: verify structured errors across commands
|
|
8
|
+
*/
|
|
9
|
+
import "dotenv/config";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import { describe, it, expect } from "vitest";
|
|
12
|
+
const CLI_CWD = "/Users/hik/Documents/GitHub/pacifica/packages/cli";
|
|
13
|
+
const CLI_CMD = "npx tsx src/index.ts";
|
|
14
|
+
function runCli(args) {
|
|
15
|
+
return execSync(`${CLI_CMD} ${args}`, {
|
|
16
|
+
encoding: "utf-8",
|
|
17
|
+
cwd: CLI_CWD,
|
|
18
|
+
timeout: 25000,
|
|
19
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function runCliSafe(args) {
|
|
23
|
+
try {
|
|
24
|
+
const stdout = execSync(`${CLI_CMD} ${args}`, {
|
|
25
|
+
encoding: "utf-8",
|
|
26
|
+
cwd: CLI_CWD,
|
|
27
|
+
timeout: 25000,
|
|
28
|
+
env: { ...process.env, NODE_NO_WARNINGS: "1" },
|
|
29
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
30
|
+
});
|
|
31
|
+
return { stdout, stderr: "", exitCode: 0 };
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const e = err;
|
|
35
|
+
return {
|
|
36
|
+
stdout: e.stdout ?? "",
|
|
37
|
+
stderr: e.stderr ?? "",
|
|
38
|
+
exitCode: e.status ?? 1,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
describe("New Commands E2E Integration", { timeout: 30000 }, () => {
|
|
43
|
+
// ══════════════════════════════════════════════════════════
|
|
44
|
+
// api-spec — no adapter needed
|
|
45
|
+
// ══════════════════════════════════════════════════════════
|
|
46
|
+
describe("perp api-spec", () => {
|
|
47
|
+
let spec;
|
|
48
|
+
it("outputs valid JSON envelope with ok:true", () => {
|
|
49
|
+
const output = runCli("api-spec");
|
|
50
|
+
spec = JSON.parse(output);
|
|
51
|
+
expect(spec.ok).toBe(true);
|
|
52
|
+
expect(spec.data).toBeDefined();
|
|
53
|
+
expect(spec.meta).toBeDefined();
|
|
54
|
+
expect(spec.meta.timestamp).toBeDefined();
|
|
55
|
+
});
|
|
56
|
+
it("data contains name, version, commands, errorCodes", () => {
|
|
57
|
+
const output = runCli("api-spec");
|
|
58
|
+
spec = JSON.parse(output);
|
|
59
|
+
const data = spec.data;
|
|
60
|
+
expect(data.name).toBe("perp");
|
|
61
|
+
expect(data.version).toBeDefined();
|
|
62
|
+
expect(data.description).toBeDefined();
|
|
63
|
+
expect(Array.isArray(data.commands)).toBe(true);
|
|
64
|
+
expect(typeof data.errorCodes).toBe("object");
|
|
65
|
+
expect(Array.isArray(data.exchanges)).toBe(true);
|
|
66
|
+
expect(Array.isArray(data.tips)).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
it("commands include all major command groups with subcommands", () => {
|
|
69
|
+
const output = runCli("api-spec");
|
|
70
|
+
spec = JSON.parse(output);
|
|
71
|
+
const data = spec.data;
|
|
72
|
+
const commands = data.commands;
|
|
73
|
+
const names = commands.map((c) => c.name);
|
|
74
|
+
expect(names).toContain("market");
|
|
75
|
+
expect(names).toContain("account");
|
|
76
|
+
expect(names).toContain("trade");
|
|
77
|
+
expect(names).toContain("arb");
|
|
78
|
+
expect(names).toContain("status");
|
|
79
|
+
expect(names).toContain("health");
|
|
80
|
+
expect(names).toContain("portfolio");
|
|
81
|
+
expect(names).toContain("risk");
|
|
82
|
+
expect(names).toContain("api-spec");
|
|
83
|
+
// market should have subcommands including mid
|
|
84
|
+
const market = commands.find((c) => c.name === "market");
|
|
85
|
+
expect(market?.subcommands).toBeDefined();
|
|
86
|
+
const marketSubs = market.subcommands.map((s) => s.name);
|
|
87
|
+
expect(marketSubs).toContain("mid");
|
|
88
|
+
expect(marketSubs).toContain("list");
|
|
89
|
+
expect(marketSubs).toContain("info");
|
|
90
|
+
expect(marketSubs).toContain("book");
|
|
91
|
+
// account should have margin subcommand
|
|
92
|
+
const account = commands.find((c) => c.name === "account");
|
|
93
|
+
const accountSubs = account.subcommands.map((s) => s.name);
|
|
94
|
+
expect(accountSubs).toContain("margin");
|
|
95
|
+
expect(accountSubs).toContain("info");
|
|
96
|
+
expect(accountSubs).toContain("positions");
|
|
97
|
+
// trade should have status and fills subcommands
|
|
98
|
+
const trade = commands.find((c) => c.name === "trade");
|
|
99
|
+
const tradeSubs = trade.subcommands.map((s) => s.name);
|
|
100
|
+
expect(tradeSubs).toContain("status");
|
|
101
|
+
expect(tradeSubs).toContain("fills");
|
|
102
|
+
});
|
|
103
|
+
it("errorCodes have consistent structure", () => {
|
|
104
|
+
const output = runCli("api-spec");
|
|
105
|
+
spec = JSON.parse(output);
|
|
106
|
+
const data = spec.data;
|
|
107
|
+
const errorCodes = data.errorCodes;
|
|
108
|
+
const codes = Object.keys(errorCodes);
|
|
109
|
+
expect(codes.length).toBeGreaterThanOrEqual(15);
|
|
110
|
+
for (const [code, info] of Object.entries(errorCodes)) {
|
|
111
|
+
expect(typeof info.status).toBe("number");
|
|
112
|
+
expect(typeof info.retryable).toBe("boolean");
|
|
113
|
+
expect(typeof info.description).toBe("string");
|
|
114
|
+
expect(info.description.length).toBeGreaterThan(0);
|
|
115
|
+
// HTTP status codes should be in valid range
|
|
116
|
+
expect(info.status).toBeGreaterThanOrEqual(400);
|
|
117
|
+
expect(info.status).toBeLessThanOrEqual(599);
|
|
118
|
+
}
|
|
119
|
+
// Retryable codes should have 5xx status
|
|
120
|
+
expect(errorCodes.EXCHANGE_UNREACHABLE.retryable).toBe(true);
|
|
121
|
+
expect(errorCodes.RATE_LIMITED.retryable).toBe(true);
|
|
122
|
+
expect(errorCodes.TIMEOUT.retryable).toBe(true);
|
|
123
|
+
// Non-retryable codes
|
|
124
|
+
expect(errorCodes.INVALID_PARAMS.retryable).toBe(false);
|
|
125
|
+
expect(errorCodes.INSUFFICIENT_BALANCE.retryable).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
it("globalOptions include --json, --exchange, --dry-run", () => {
|
|
128
|
+
const output = runCli("api-spec");
|
|
129
|
+
spec = JSON.parse(output);
|
|
130
|
+
const data = spec.data;
|
|
131
|
+
const opts = data.globalOptions;
|
|
132
|
+
const allFlags = opts.map((o) => o.flags).join(" ");
|
|
133
|
+
expect(allFlags).toContain("--json");
|
|
134
|
+
expect(allFlags).toContain("--exchange");
|
|
135
|
+
expect(allFlags).toContain("--dry-run");
|
|
136
|
+
expect(allFlags).toContain("--dex");
|
|
137
|
+
});
|
|
138
|
+
it("tips array includes referral nudge", () => {
|
|
139
|
+
const output = runCli("api-spec");
|
|
140
|
+
spec = JSON.parse(output);
|
|
141
|
+
const data = spec.data;
|
|
142
|
+
const tips = data.tips;
|
|
143
|
+
expect(tips.length).toBeGreaterThanOrEqual(5);
|
|
144
|
+
const joined = tips.join("\n");
|
|
145
|
+
expect(joined).toContain("referrals");
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
// ══════════════════════════════════════════════════════════
|
|
149
|
+
// market mid — uses HL mainnet read-only (public data)
|
|
150
|
+
// ══════════════════════════════════════════════════════════
|
|
151
|
+
const HAS_KEY = !!(process.env.HYPERLIQUID_PRIVATE_KEY || process.env.HL_PRIVATE_KEY);
|
|
152
|
+
describe.skipIf(!HAS_KEY)("perp --json -e hyperliquid market mid BTC", () => {
|
|
153
|
+
it("returns valid mid price envelope", () => {
|
|
154
|
+
const output = runCli("--json -e hyperliquid market mid BTC");
|
|
155
|
+
const parsed = JSON.parse(output);
|
|
156
|
+
expect(parsed.ok).toBe(true);
|
|
157
|
+
expect(parsed.meta?.timestamp).toBeDefined();
|
|
158
|
+
const data = parsed.data;
|
|
159
|
+
expect(data.symbol).toBe("BTC");
|
|
160
|
+
expect(typeof data.mid).toBe("string");
|
|
161
|
+
expect(typeof data.bid).toBe("string");
|
|
162
|
+
expect(typeof data.ask).toBe("string");
|
|
163
|
+
expect(typeof data.spread).toBe("string");
|
|
164
|
+
// Price sanity
|
|
165
|
+
const mid = parseFloat(data.mid);
|
|
166
|
+
expect(mid).toBeGreaterThan(100);
|
|
167
|
+
const bid = parseFloat(data.bid);
|
|
168
|
+
const ask = parseFloat(data.ask);
|
|
169
|
+
expect(bid).toBeLessThan(ask);
|
|
170
|
+
expect(bid).toBeGreaterThan(0);
|
|
171
|
+
// Spread should be tiny for BTC
|
|
172
|
+
const spread = parseFloat(data.spread);
|
|
173
|
+
expect(spread).toBeGreaterThanOrEqual(0);
|
|
174
|
+
expect(spread).toBeLessThan(1); // < 1%
|
|
175
|
+
});
|
|
176
|
+
it("returns ETH mid price with reasonable values", () => {
|
|
177
|
+
const output = runCli("--json -e hyperliquid market mid ETH");
|
|
178
|
+
const parsed = JSON.parse(output);
|
|
179
|
+
expect(parsed.ok).toBe(true);
|
|
180
|
+
const mid = parseFloat(parsed.data.mid);
|
|
181
|
+
expect(mid).toBeGreaterThan(10);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// ══════════════════════════════════════════════════════════
|
|
185
|
+
// Error envelope consistency
|
|
186
|
+
// ══════════════════════════════════════════════════════════
|
|
187
|
+
describe("--json error envelope consistency", () => {
|
|
188
|
+
it("unknown command returns CLI_ERROR with meta.timestamp", () => {
|
|
189
|
+
const { stdout } = runCliSafe("--json fakecmd123");
|
|
190
|
+
const parsed = JSON.parse(stdout);
|
|
191
|
+
expect(parsed.ok).toBe(false);
|
|
192
|
+
expect(parsed.error).toBeDefined();
|
|
193
|
+
expect(parsed.error.code).toBe("CLI_ERROR");
|
|
194
|
+
expect(typeof parsed.error.message).toBe("string");
|
|
195
|
+
expect(parsed.meta).toBeDefined();
|
|
196
|
+
expect(parsed.meta.timestamp).toBeDefined();
|
|
197
|
+
// Timestamp should be valid ISO 8601
|
|
198
|
+
expect(new Date(parsed.meta.timestamp).toISOString()).toBe(parsed.meta.timestamp);
|
|
199
|
+
});
|
|
200
|
+
it("plan validate with nonexistent file returns structured error", () => {
|
|
201
|
+
const { stdout } = runCliSafe("--json plan validate /tmp/__nonexistent_99999.json");
|
|
202
|
+
const parsed = JSON.parse(stdout);
|
|
203
|
+
expect(parsed.ok).toBe(false);
|
|
204
|
+
expect(parsed.error.code).toBeDefined();
|
|
205
|
+
expect(parsed.error.message).toContain("ENOENT");
|
|
206
|
+
expect(parsed.meta.timestamp).toBeDefined();
|
|
207
|
+
});
|
|
208
|
+
it("api-spec always returns ok:true even without --json flag", () => {
|
|
209
|
+
const output = runCli("api-spec");
|
|
210
|
+
const parsed = JSON.parse(output);
|
|
211
|
+
expect(parsed.ok).toBe(true);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
// ══════════════════════════════════════════════════════════
|
|
215
|
+
// help output validation
|
|
216
|
+
// ══════════════════════════════════════════════════════════
|
|
217
|
+
describe("help output includes new commands", () => {
|
|
218
|
+
it("market --help lists mid subcommand", () => {
|
|
219
|
+
const { stdout } = runCliSafe("market --help");
|
|
220
|
+
expect(stdout).toContain("mid");
|
|
221
|
+
});
|
|
222
|
+
it("account --help lists margin subcommand", () => {
|
|
223
|
+
const { stdout } = runCliSafe("account --help");
|
|
224
|
+
expect(stdout).toContain("margin");
|
|
225
|
+
});
|
|
226
|
+
it("trade --help lists status and fills subcommands", () => {
|
|
227
|
+
const { stdout } = runCliSafe("trade --help");
|
|
228
|
+
expect(stdout).toContain("status");
|
|
229
|
+
expect(stdout).toContain("fills");
|
|
230
|
+
});
|
|
231
|
+
it("top-level --help lists api-spec", () => {
|
|
232
|
+
const { stdout } = runCliSafe("--help");
|
|
233
|
+
expect(stdout).toContain("api-spec");
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests: Order Verification against Hyperliquid Mainnet
|
|
3
|
+
*
|
|
4
|
+
* READ-ONLY tests — no orders are placed, no funds are touched.
|
|
5
|
+
* Uses a dummy private key to instantiate the adapter for market data queries.
|
|
6
|
+
* All assertions use reasonable ranges to accommodate live price fluctuations.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
9
|
+
import { HyperliquidAdapter } from "../../exchanges/hyperliquid.js";
|
|
10
|
+
import { validateTrade } from "../../trade-validator.js";
|
|
11
|
+
// Dummy key — only used for read-only info queries, never signs a transaction
|
|
12
|
+
const DUMMY_KEY = "0x" + "1".repeat(64);
|
|
13
|
+
/** Find a market by base symbol, tolerating a -PERP suffix. */
|
|
14
|
+
function findMarket(markets, base) {
|
|
15
|
+
const b = base.toUpperCase();
|
|
16
|
+
return markets.find((m) => m.symbol.toUpperCase() === b ||
|
|
17
|
+
m.symbol.toUpperCase() === `${b}-PERP` ||
|
|
18
|
+
m.symbol.toUpperCase().replace(/-PERP$/, "") === b);
|
|
19
|
+
}
|
|
20
|
+
describe("Hyperliquid mainnet — order verification (read-only)", () => {
|
|
21
|
+
let adapter;
|
|
22
|
+
let markets;
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
adapter = new HyperliquidAdapter(DUMMY_KEY, false);
|
|
25
|
+
await adapter.init();
|
|
26
|
+
markets = await adapter.getMarkets();
|
|
27
|
+
// Sanity: the exchange must expose at least a handful of markets
|
|
28
|
+
expect(markets.length).toBeGreaterThan(10);
|
|
29
|
+
}, 30_000);
|
|
30
|
+
// ────────────────────────────────────────────────────────────
|
|
31
|
+
// 1. "Buy 0.001 BTC" — validation with real data
|
|
32
|
+
// ────────────────────────────────────────────────────────────
|
|
33
|
+
it("validates a 0.001 BTC market buy with sane estimates", async () => {
|
|
34
|
+
const result = await validateTrade(adapter, {
|
|
35
|
+
symbol: "BTC",
|
|
36
|
+
side: "buy",
|
|
37
|
+
size: 0.001,
|
|
38
|
+
type: "market",
|
|
39
|
+
});
|
|
40
|
+
// Symbol must be found
|
|
41
|
+
const symCheck = result.checks.find((c) => c.check === "symbol_valid");
|
|
42
|
+
expect(symCheck).toBeDefined();
|
|
43
|
+
expect(symCheck.passed).toBe(true);
|
|
44
|
+
// Mark price: BTC should be well above $10 000
|
|
45
|
+
expect(result.marketInfo).toBeDefined();
|
|
46
|
+
expect(result.marketInfo.markPrice).toBeGreaterThan(10_000);
|
|
47
|
+
// Estimated cost: 0.001 BTC at ~$60-120k → notional $60-$120
|
|
48
|
+
// Margin at max leverage (~50x) ≈ $1.2-$2.4
|
|
49
|
+
// Fee at 0.05% ≈ $0.03-$0.06
|
|
50
|
+
expect(result.estimatedCost).toBeDefined();
|
|
51
|
+
expect(result.estimatedCost.margin).toBeGreaterThan(0.5);
|
|
52
|
+
expect(result.estimatedCost.margin).toBeLessThan(20);
|
|
53
|
+
expect(result.estimatedCost.fee).toBeGreaterThan(0.01);
|
|
54
|
+
expect(result.estimatedCost.fee).toBeLessThan(1);
|
|
55
|
+
}, 30_000);
|
|
56
|
+
// ────────────────────────────────────────────────────────────
|
|
57
|
+
// 2. "Buy 1 ETH" — cost estimation accuracy
|
|
58
|
+
// ────────────────────────────────────────────────────────────
|
|
59
|
+
it("validates a 1 ETH market buy with accurate cost estimation", async () => {
|
|
60
|
+
const result = await validateTrade(adapter, {
|
|
61
|
+
symbol: "ETH",
|
|
62
|
+
side: "buy",
|
|
63
|
+
size: 1.0,
|
|
64
|
+
type: "market",
|
|
65
|
+
});
|
|
66
|
+
expect(result.marketInfo).toBeDefined();
|
|
67
|
+
const ethPrice = result.marketInfo.markPrice;
|
|
68
|
+
// ETH price should be in a broad but reasonable range
|
|
69
|
+
expect(ethPrice).toBeGreaterThan(500);
|
|
70
|
+
expect(ethPrice).toBeLessThan(20_000);
|
|
71
|
+
// Margin at max leverage: notional / maxLev
|
|
72
|
+
// At 50x: margin ~$36-$400 depending on price
|
|
73
|
+
expect(result.estimatedCost).toBeDefined();
|
|
74
|
+
expect(result.estimatedCost.margin).toBeGreaterThan(5);
|
|
75
|
+
expect(result.estimatedCost.margin).toBeLessThan(500);
|
|
76
|
+
// Fee: 0.05% of notional ($500-$20k) → $0.25 - $10
|
|
77
|
+
expect(result.estimatedCost.fee).toBeGreaterThan(0.1);
|
|
78
|
+
expect(result.estimatedCost.fee).toBeLessThan(15);
|
|
79
|
+
}, 30_000);
|
|
80
|
+
// ────────────────────────────────────────────────────────────
|
|
81
|
+
// 3. "Buy 0.1 SOL" — smaller asset
|
|
82
|
+
// ────────────────────────────────────────────────────────────
|
|
83
|
+
it("validates a 0.1 SOL buy — smaller asset sanity", async () => {
|
|
84
|
+
const result = await validateTrade(adapter, {
|
|
85
|
+
symbol: "SOL",
|
|
86
|
+
side: "buy",
|
|
87
|
+
size: 0.1,
|
|
88
|
+
type: "market",
|
|
89
|
+
});
|
|
90
|
+
const symCheck = result.checks.find((c) => c.check === "symbol_valid");
|
|
91
|
+
expect(symCheck).toBeDefined();
|
|
92
|
+
expect(symCheck.passed).toBe(true);
|
|
93
|
+
expect(result.marketInfo).toBeDefined();
|
|
94
|
+
expect(result.marketInfo.markPrice).toBeGreaterThan(10);
|
|
95
|
+
// Notional ~$1-$100 at current prices — cost should be small but positive
|
|
96
|
+
expect(result.estimatedCost).toBeDefined();
|
|
97
|
+
expect(result.estimatedCost.margin).toBeGreaterThan(0);
|
|
98
|
+
expect(result.estimatedCost.fee).toBeGreaterThan(0);
|
|
99
|
+
}, 30_000);
|
|
100
|
+
// ────────────────────────────────────────────────────────────
|
|
101
|
+
// 4. "Sell 100 BTC" — liquidity / slippage check
|
|
102
|
+
// ────────────────────────────────────────────────────────────
|
|
103
|
+
it("detects liquidity/slippage concern for a 100 BTC sell", async () => {
|
|
104
|
+
const result = await validateTrade(adapter, {
|
|
105
|
+
symbol: "BTC",
|
|
106
|
+
side: "sell",
|
|
107
|
+
size: 100,
|
|
108
|
+
type: "market",
|
|
109
|
+
});
|
|
110
|
+
// 100 BTC ≈ $6-12M notional — the validator should either:
|
|
111
|
+
// a) flag insufficient liquidity in the visible orderbook, or
|
|
112
|
+
// b) flag high slippage, or
|
|
113
|
+
// c) flag a risk-limits violation (max position size $5k default)
|
|
114
|
+
const liqCheck = result.checks.find((c) => c.check === "liquidity_ok");
|
|
115
|
+
const riskCheck = result.checks.find((c) => c.check === "risk_limits");
|
|
116
|
+
// At least one of these should flag the absurd size
|
|
117
|
+
const anyFlagged = (liqCheck && !liqCheck.passed) || (riskCheck && !riskCheck.passed);
|
|
118
|
+
expect(anyFlagged).toBe(true);
|
|
119
|
+
}, 30_000);
|
|
120
|
+
// ────────────────────────────────────────────────────────────
|
|
121
|
+
// 5. Orderbook consistency — bid < ask
|
|
122
|
+
// ────────────────────────────────────────────────────────────
|
|
123
|
+
it("verifies orderbook bid < ask and spread near mark for BTC, ETH, SOL", async () => {
|
|
124
|
+
const symbols = ["BTC", "ETH", "SOL"];
|
|
125
|
+
for (const sym of symbols) {
|
|
126
|
+
const book = await adapter.getOrderbook(sym);
|
|
127
|
+
// Must have at least one level on each side
|
|
128
|
+
expect(book.bids.length).toBeGreaterThan(0);
|
|
129
|
+
expect(book.asks.length).toBeGreaterThan(0);
|
|
130
|
+
const bestBid = Number(book.bids[0][0]);
|
|
131
|
+
const bestAsk = Number(book.asks[0][0]);
|
|
132
|
+
// Best bid must be strictly less than best ask (positive spread)
|
|
133
|
+
expect(bestBid).toBeLessThan(bestAsk);
|
|
134
|
+
// Spread should be tiny relative to price (< 0.1%)
|
|
135
|
+
const spread = (bestAsk - bestBid) / bestBid;
|
|
136
|
+
expect(spread).toBeLessThan(0.001); // 0.1%
|
|
137
|
+
// Both sides should be near the mark price (within 0.1%)
|
|
138
|
+
const market = findMarket(markets, sym);
|
|
139
|
+
expect(market).toBeDefined();
|
|
140
|
+
const mark = Number(market.markPrice);
|
|
141
|
+
if (mark > 0) {
|
|
142
|
+
expect(Math.abs(bestBid - mark) / mark).toBeLessThan(0.001);
|
|
143
|
+
expect(Math.abs(bestAsk - mark) / mark).toBeLessThan(0.001);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}, 30_000);
|
|
147
|
+
// ────────────────────────────────────────────────────────────
|
|
148
|
+
// 6. Position side mapping — close logic
|
|
149
|
+
// ────────────────────────────────────────────────────────────
|
|
150
|
+
it("maps position close side correctly (long → sell, short → buy)", () => {
|
|
151
|
+
// Pure logic check using real market context types
|
|
152
|
+
const scenarios = [
|
|
153
|
+
{ side: "long", expectedClose: "sell" },
|
|
154
|
+
{ side: "short", expectedClose: "buy" },
|
|
155
|
+
];
|
|
156
|
+
for (const { side, expectedClose } of scenarios) {
|
|
157
|
+
// The universal close-position rule: to close, you place
|
|
158
|
+
// the opposite side order.
|
|
159
|
+
const closeSide = side === "long" ? "sell" : "buy";
|
|
160
|
+
expect(closeSide).toBe(expectedClose);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
// ────────────────────────────────────────────────────────────
|
|
164
|
+
// 7. Leverage bounds from real API
|
|
165
|
+
// ────────────────────────────────────────────────────────────
|
|
166
|
+
it("reports maxLeverage >= 20 for BTC and ETH, lower for small-caps", () => {
|
|
167
|
+
const btc = findMarket(markets, "BTC");
|
|
168
|
+
const eth = findMarket(markets, "ETH");
|
|
169
|
+
expect(btc).toBeDefined();
|
|
170
|
+
expect(eth).toBeDefined();
|
|
171
|
+
// HL allows 40-50x on BTC/ETH
|
|
172
|
+
expect(btc.maxLeverage).toBeGreaterThanOrEqual(20);
|
|
173
|
+
expect(eth.maxLeverage).toBeGreaterThanOrEqual(20);
|
|
174
|
+
// Find a lower-liquidity asset (take the last market, or one with low volume)
|
|
175
|
+
const lowCap = markets
|
|
176
|
+
.filter((m) => m.symbol !== "BTC" &&
|
|
177
|
+
m.symbol !== "ETH" &&
|
|
178
|
+
m.symbol !== "SOL" &&
|
|
179
|
+
Number(m.volume24h) > 0)
|
|
180
|
+
.sort((a, b) => Number(a.volume24h) - Number(b.volume24h))[0];
|
|
181
|
+
if (lowCap) {
|
|
182
|
+
// Low-cap coins typically have lower max leverage than BTC
|
|
183
|
+
// Allow equality since some may still be 50x; the key check is that the
|
|
184
|
+
// field is populated and numeric
|
|
185
|
+
expect(lowCap.maxLeverage).toBeGreaterThan(0);
|
|
186
|
+
expect(lowCap.maxLeverage).toBeLessThanOrEqual(btc.maxLeverage);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
// ────────────────────────────────────────────────────────────
|
|
190
|
+
// 8. Funding rate sanity
|
|
191
|
+
// ────────────────────────────────────────────────────────────
|
|
192
|
+
it("returns funding rates in -1% to 1% range for BTC and ETH", () => {
|
|
193
|
+
const btc = findMarket(markets, "BTC");
|
|
194
|
+
const eth = findMarket(markets, "ETH");
|
|
195
|
+
expect(btc).toBeDefined();
|
|
196
|
+
expect(eth).toBeDefined();
|
|
197
|
+
const btcFunding = Number(btc.fundingRate);
|
|
198
|
+
const ethFunding = Number(eth.fundingRate);
|
|
199
|
+
// Hourly funding rates should be tiny decimals (e.g., 0.0001 = 0.01%)
|
|
200
|
+
// Anything outside +-1% would indicate a parsing error
|
|
201
|
+
expect(btcFunding).toBeGreaterThan(-0.01);
|
|
202
|
+
expect(btcFunding).toBeLessThan(0.01);
|
|
203
|
+
expect(ethFunding).toBeGreaterThan(-0.01);
|
|
204
|
+
expect(ethFunding).toBeLessThan(0.01);
|
|
205
|
+
});
|
|
206
|
+
// ────────────────────────────────────────────────────────────
|
|
207
|
+
// 9. Invalid symbol rejection
|
|
208
|
+
// ────────────────────────────────────────────────────────────
|
|
209
|
+
it("rejects an invalid symbol (XYZNOTREAL999)", async () => {
|
|
210
|
+
const result = await validateTrade(adapter, {
|
|
211
|
+
symbol: "XYZNOTREAL999",
|
|
212
|
+
side: "buy",
|
|
213
|
+
size: 1,
|
|
214
|
+
type: "market",
|
|
215
|
+
});
|
|
216
|
+
expect(result.valid).toBe(false);
|
|
217
|
+
const symCheck = result.checks.find((c) => c.check === "symbol_valid");
|
|
218
|
+
expect(symCheck).toBeDefined();
|
|
219
|
+
expect(symCheck.passed).toBe(false);
|
|
220
|
+
expect(symCheck.message).toContain("not found");
|
|
221
|
+
}, 30_000);
|
|
222
|
+
it("handles an empty symbol gracefully", async () => {
|
|
223
|
+
const result = await validateTrade(adapter, {
|
|
224
|
+
symbol: "",
|
|
225
|
+
side: "buy",
|
|
226
|
+
size: 1,
|
|
227
|
+
type: "market",
|
|
228
|
+
});
|
|
229
|
+
expect(result.valid).toBe(false);
|
|
230
|
+
const symCheck = result.checks.find((c) => c.check === "symbol_valid");
|
|
231
|
+
expect(symCheck).toBeDefined();
|
|
232
|
+
expect(symCheck.passed).toBe(false);
|
|
233
|
+
}, 30_000);
|
|
234
|
+
// ────────────────────────────────────────────────────────────
|
|
235
|
+
// 10. Reduce-only without position
|
|
236
|
+
// ────────────────────────────────────────────────────────────
|
|
237
|
+
it("rejects reduce-only when no position exists (dummy account)", async () => {
|
|
238
|
+
const result = await validateTrade(adapter, {
|
|
239
|
+
symbol: "BTC",
|
|
240
|
+
side: "sell",
|
|
241
|
+
size: 0.01,
|
|
242
|
+
type: "market",
|
|
243
|
+
reduceOnly: true,
|
|
244
|
+
});
|
|
245
|
+
// The dummy key has no positions, so the position_exists check should fail
|
|
246
|
+
const posCheck = result.checks.find((c) => c.check === "position_exists");
|
|
247
|
+
expect(posCheck).toBeDefined();
|
|
248
|
+
expect(posCheck.passed).toBe(false);
|
|
249
|
+
expect(posCheck.message).toContain("No position found");
|
|
250
|
+
expect(posCheck.message).toContain("reduce-only");
|
|
251
|
+
}, 30_000);
|
|
252
|
+
// ────────────────────────────────────────────────────────────
|
|
253
|
+
// 11. Price freshness with real data
|
|
254
|
+
// ────────────────────────────────────────────────────────────
|
|
255
|
+
it("passes price freshness at mark +-1% and fails at +-15%", async () => {
|
|
256
|
+
// Fetch the current BTC mark price
|
|
257
|
+
const btcMarket = findMarket(markets, "BTC");
|
|
258
|
+
expect(btcMarket).toBeDefined();
|
|
259
|
+
const mark = Number(btcMarket.markPrice);
|
|
260
|
+
expect(mark).toBeGreaterThan(10_000);
|
|
261
|
+
// Limit order at mark + 1% → should pass freshness
|
|
262
|
+
const closeResult = await validateTrade(adapter, {
|
|
263
|
+
symbol: "BTC",
|
|
264
|
+
side: "buy",
|
|
265
|
+
size: 0.001,
|
|
266
|
+
type: "limit",
|
|
267
|
+
price: mark * 1.01,
|
|
268
|
+
});
|
|
269
|
+
const closeFresh = closeResult.checks.find((c) => c.check === "price_fresh");
|
|
270
|
+
expect(closeFresh).toBeDefined();
|
|
271
|
+
expect(closeFresh.passed).toBe(true);
|
|
272
|
+
// Limit order at mark + 15% → should fail freshness (>10% deviation)
|
|
273
|
+
const farResult = await validateTrade(adapter, {
|
|
274
|
+
symbol: "BTC",
|
|
275
|
+
side: "buy",
|
|
276
|
+
size: 0.001,
|
|
277
|
+
type: "limit",
|
|
278
|
+
price: mark * 1.15,
|
|
279
|
+
});
|
|
280
|
+
const farFresh = farResult.checks.find((c) => c.check === "price_fresh");
|
|
281
|
+
expect(farFresh).toBeDefined();
|
|
282
|
+
expect(farFresh.passed).toBe(false);
|
|
283
|
+
expect(farFresh.message).toContain("deviates");
|
|
284
|
+
}, 30_000);
|
|
285
|
+
// ────────────────────────────────────────────────────────────
|
|
286
|
+
// 12. Cross-exchange spread: native HL vs HIP-3 dex
|
|
287
|
+
// ────────────────────────────────────────────────────────────
|
|
288
|
+
it("compares BTC price on native HL against a HIP-3 dex (hyna)", async () => {
|
|
289
|
+
// Native HL BTC price
|
|
290
|
+
const btcNative = findMarket(markets, "BTC");
|
|
291
|
+
expect(btcNative).toBeDefined();
|
|
292
|
+
const nativePrice = Number(btcNative.markPrice);
|
|
293
|
+
expect(nativePrice).toBeGreaterThan(10_000);
|
|
294
|
+
// Try to get hyna dex markets — if dex doesn't exist or BTC isn't listed,
|
|
295
|
+
// skip gracefully (the dex landscape changes over time).
|
|
296
|
+
let dexPrice = null;
|
|
297
|
+
try {
|
|
298
|
+
const dexAdapter = new HyperliquidAdapter(DUMMY_KEY, false);
|
|
299
|
+
await dexAdapter.init();
|
|
300
|
+
dexAdapter.setDex("hyna");
|
|
301
|
+
// Re-init asset map for the dex context
|
|
302
|
+
const dexMarkets = await dexAdapter.getMarkets();
|
|
303
|
+
const btcDex = findMarket(dexMarkets, "BTC");
|
|
304
|
+
if (btcDex) {
|
|
305
|
+
dexPrice = Number(btcDex.markPrice);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
// hyna dex may not exist — skip comparison
|
|
310
|
+
}
|
|
311
|
+
if (dexPrice !== null && dexPrice > 0) {
|
|
312
|
+
// Prices should be within 0.5% of each other (same underlying)
|
|
313
|
+
const divergence = Math.abs(nativePrice - dexPrice) / nativePrice;
|
|
314
|
+
expect(divergence).toBeLessThan(0.005);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
// No comparable dex market — test passes vacuously with a note
|
|
318
|
+
console.log("Skipped cross-dex comparison: hyna BTC market not available");
|
|
319
|
+
}
|
|
320
|
+
}, 30_000);
|
|
321
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from "vitest";
|
|
2
|
+
import { PacificaAdapter } from "../../exchanges/pacifica.js";
|
|
3
|
+
import { parseSolanaKeypair } from "../../config.js";
|
|
4
|
+
/**
|
|
5
|
+
* Integration tests for Pacifica exchange adapter (Solana Devnet).
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* 1. Set PACIFICA_PRIVATE_KEY env var (base58 or JSON array)
|
|
9
|
+
* 2. Have devnet SOL: solana airdrop 2 --url devnet
|
|
10
|
+
* 3. Have devnet USDC deposited into Pacifica testnet
|
|
11
|
+
*
|
|
12
|
+
* Run: PACIFICA_PRIVATE_KEY=<key> pnpm --filter perp-cli test -- --testPathPattern integration/pacifica
|
|
13
|
+
*/
|
|
14
|
+
const SKIP = !process.env.PACIFICA_PRIVATE_KEY;
|
|
15
|
+
describe.skipIf(SKIP)("Pacifica Integration (Devnet)", () => {
|
|
16
|
+
let adapter;
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
const keypair = parseSolanaKeypair(process.env.PACIFICA_PRIVATE_KEY);
|
|
19
|
+
adapter = new PacificaAdapter(keypair, "testnet");
|
|
20
|
+
});
|
|
21
|
+
describe("Read-only operations", () => {
|
|
22
|
+
it("fetches markets", async () => {
|
|
23
|
+
const markets = await adapter.getMarkets();
|
|
24
|
+
expect(markets.length).toBeGreaterThan(0);
|
|
25
|
+
expect(markets[0].symbol).toBeTruthy();
|
|
26
|
+
expect(Number(markets[0].maxLeverage)).toBeGreaterThan(0);
|
|
27
|
+
});
|
|
28
|
+
it("fetches orderbook for BTC", async () => {
|
|
29
|
+
const book = await adapter.getOrderbook("BTC");
|
|
30
|
+
expect(book).toHaveProperty("bids");
|
|
31
|
+
expect(book).toHaveProperty("asks");
|
|
32
|
+
});
|
|
33
|
+
it("fetches balance", async () => {
|
|
34
|
+
const balance = await adapter.getBalance();
|
|
35
|
+
expect(balance).toHaveProperty("equity");
|
|
36
|
+
expect(balance).toHaveProperty("available");
|
|
37
|
+
expect(Number(balance.equity)).toBeGreaterThanOrEqual(0);
|
|
38
|
+
});
|
|
39
|
+
it("fetches positions", async () => {
|
|
40
|
+
const positions = await adapter.getPositions();
|
|
41
|
+
expect(Array.isArray(positions)).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it("fetches open orders", async () => {
|
|
44
|
+
const orders = await adapter.getOpenOrders();
|
|
45
|
+
expect(Array.isArray(orders)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
describe("Trading operations", () => {
|
|
49
|
+
// WARNING: These tests place REAL orders on testnet
|
|
50
|
+
const TEST_SYMBOL = "BTC";
|
|
51
|
+
let limitOrderId;
|
|
52
|
+
it("places a limit buy order (far from market)", async () => {
|
|
53
|
+
// Place far below market so it won't fill
|
|
54
|
+
const result = await adapter.limitOrder(TEST_SYMBOL, "buy", "10000", "0.001");
|
|
55
|
+
expect(result).toBeTruthy();
|
|
56
|
+
// Check it appears in open orders
|
|
57
|
+
const orders = await adapter.getOpenOrders();
|
|
58
|
+
const myOrder = orders.find((o) => o.symbol === TEST_SYMBOL && o.side === "buy" && o.price === "10000");
|
|
59
|
+
if (myOrder) {
|
|
60
|
+
limitOrderId = myOrder.orderId;
|
|
61
|
+
expect(myOrder.status).toBe("open");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
it("cancels the limit order", async () => {
|
|
65
|
+
if (!limitOrderId)
|
|
66
|
+
return;
|
|
67
|
+
const result = await adapter.cancelOrder(TEST_SYMBOL, limitOrderId);
|
|
68
|
+
expect(result).toBeTruthy();
|
|
69
|
+
});
|
|
70
|
+
it("cancel all orders succeeds", async () => {
|
|
71
|
+
const result = await adapter.cancelAllOrders();
|
|
72
|
+
expect(result).toBeTruthy();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|