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,137 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { existsSync, rmSync, mkdirSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import { generateClientId, logClientId, lookupClientId, isOrderDuplicate, updateClientId, readClientIds, } from "../client-id-tracker.js";
|
|
5
|
+
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
6
|
+
const CLIENT_IDS_FILE = resolve(PERP_DIR, "client-ids.jsonl");
|
|
7
|
+
// Backup and restore any existing file to avoid test pollution
|
|
8
|
+
let backupContent = null;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
if (existsSync(CLIENT_IDS_FILE)) {
|
|
11
|
+
backupContent = require("fs").readFileSync(CLIENT_IDS_FILE, "utf-8");
|
|
12
|
+
rmSync(CLIENT_IDS_FILE);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (existsSync(CLIENT_IDS_FILE))
|
|
17
|
+
rmSync(CLIENT_IDS_FILE);
|
|
18
|
+
if (backupContent !== null) {
|
|
19
|
+
if (!existsSync(PERP_DIR))
|
|
20
|
+
mkdirSync(PERP_DIR, { recursive: true });
|
|
21
|
+
require("fs").writeFileSync(CLIENT_IDS_FILE, backupContent);
|
|
22
|
+
backupContent = null;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
function makeRecord(overrides) {
|
|
26
|
+
return {
|
|
27
|
+
clientOrderId: generateClientId(),
|
|
28
|
+
exchange: "test",
|
|
29
|
+
symbol: "BTC",
|
|
30
|
+
side: "buy",
|
|
31
|
+
size: "0.1",
|
|
32
|
+
type: "market",
|
|
33
|
+
status: "pending",
|
|
34
|
+
createdAt: new Date().toISOString(),
|
|
35
|
+
updatedAt: new Date().toISOString(),
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
describe("generateClientId", () => {
|
|
40
|
+
it("generates unique IDs", () => {
|
|
41
|
+
const ids = new Set();
|
|
42
|
+
for (let i = 0; i < 100; i++) {
|
|
43
|
+
ids.add(generateClientId());
|
|
44
|
+
}
|
|
45
|
+
expect(ids.size).toBe(100);
|
|
46
|
+
});
|
|
47
|
+
it("uses prefix when provided", () => {
|
|
48
|
+
const id = generateClientId("test");
|
|
49
|
+
expect(id.startsWith("test-")).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it("defaults to 'perp-' prefix", () => {
|
|
52
|
+
const id = generateClientId();
|
|
53
|
+
expect(id.startsWith("perp-")).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("logClientId + lookupClientId", () => {
|
|
57
|
+
it("logs and retrieves a record", () => {
|
|
58
|
+
const record = makeRecord({ clientOrderId: "test-lookup-1" });
|
|
59
|
+
logClientId(record);
|
|
60
|
+
const found = lookupClientId("test-lookup-1");
|
|
61
|
+
expect(found).not.toBeNull();
|
|
62
|
+
expect(found.clientOrderId).toBe("test-lookup-1");
|
|
63
|
+
expect(found.exchange).toBe("test");
|
|
64
|
+
expect(found.symbol).toBe("BTC");
|
|
65
|
+
});
|
|
66
|
+
it("returns null for non-existent ID", () => {
|
|
67
|
+
expect(lookupClientId("nonexistent")).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
it("returns latest record for duplicate IDs", () => {
|
|
70
|
+
const id = "test-latest";
|
|
71
|
+
logClientId(makeRecord({ clientOrderId: id, status: "pending" }));
|
|
72
|
+
logClientId(makeRecord({ clientOrderId: id, status: "filled" }));
|
|
73
|
+
const found = lookupClientId(id);
|
|
74
|
+
expect(found.status).toBe("filled");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe("isOrderDuplicate", () => {
|
|
78
|
+
it("returns false for unknown ID", () => {
|
|
79
|
+
expect(isOrderDuplicate("unknown-123")).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
it("returns true for pending order", () => {
|
|
82
|
+
const id = "test-dup-pending";
|
|
83
|
+
logClientId(makeRecord({ clientOrderId: id, status: "pending" }));
|
|
84
|
+
expect(isOrderDuplicate(id)).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
it("returns true for submitted order", () => {
|
|
87
|
+
const id = "test-dup-submitted";
|
|
88
|
+
logClientId(makeRecord({ clientOrderId: id, status: "submitted" }));
|
|
89
|
+
expect(isOrderDuplicate(id)).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
it("returns true for filled order", () => {
|
|
92
|
+
const id = "test-dup-filled";
|
|
93
|
+
logClientId(makeRecord({ clientOrderId: id, status: "filled" }));
|
|
94
|
+
expect(isOrderDuplicate(id)).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it("returns false for failed order (can be retried)", () => {
|
|
97
|
+
const id = "test-dup-failed";
|
|
98
|
+
logClientId(makeRecord({ clientOrderId: id, status: "failed" }));
|
|
99
|
+
expect(isOrderDuplicate(id)).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe("updateClientId", () => {
|
|
103
|
+
it("appends updated record", () => {
|
|
104
|
+
const id = "test-update-1";
|
|
105
|
+
logClientId(makeRecord({ clientOrderId: id, status: "pending" }));
|
|
106
|
+
updateClientId(id, { status: "filled", exchangeOrderId: "ex-123" });
|
|
107
|
+
const found = lookupClientId(id);
|
|
108
|
+
expect(found.status).toBe("filled");
|
|
109
|
+
expect(found.exchangeOrderId).toBe("ex-123");
|
|
110
|
+
});
|
|
111
|
+
it("no-ops for nonexistent ID", () => {
|
|
112
|
+
updateClientId("ghost-id", { status: "filled" });
|
|
113
|
+
expect(lookupClientId("ghost-id")).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("readClientIds", () => {
|
|
117
|
+
it("returns empty array when no file", () => {
|
|
118
|
+
expect(readClientIds()).toEqual([]);
|
|
119
|
+
});
|
|
120
|
+
it("returns all records", () => {
|
|
121
|
+
logClientId(makeRecord({ clientOrderId: "r1" }));
|
|
122
|
+
logClientId(makeRecord({ clientOrderId: "r2" }));
|
|
123
|
+
logClientId(makeRecord({ clientOrderId: "r3" }));
|
|
124
|
+
const records = readClientIds();
|
|
125
|
+
expect(records.length).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
it("respects limit parameter", () => {
|
|
128
|
+
for (let i = 0; i < 10; i++) {
|
|
129
|
+
logClientId(makeRecord({ clientOrderId: `batch-${i}` }));
|
|
130
|
+
}
|
|
131
|
+
const records = readClientIds(3);
|
|
132
|
+
expect(records.length).toBe(3);
|
|
133
|
+
// Should be the last 3
|
|
134
|
+
expect(records[0].clientOrderId).toBe("batch-7");
|
|
135
|
+
expect(records[2].clientOrderId).toBe("batch-9");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { registerMarketCommands } from "../../commands/market.js";
|
|
4
|
+
import { registerAccountCommands } from "../../commands/account.js";
|
|
5
|
+
import { registerTradeCommands } from "../../commands/trade.js";
|
|
6
|
+
// ── Mock dependencies ──
|
|
7
|
+
vi.mock("../../execution-log.js", () => ({
|
|
8
|
+
logExecution: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("../../client-id-tracker.js", () => ({
|
|
11
|
+
generateClientId: vi.fn(() => "test-id-123"),
|
|
12
|
+
logClientId: vi.fn(),
|
|
13
|
+
isOrderDuplicate: vi.fn(() => false),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../../trade-validator.js", () => ({
|
|
16
|
+
validateTrade: vi.fn().mockResolvedValue({
|
|
17
|
+
valid: true,
|
|
18
|
+
checks: [],
|
|
19
|
+
warnings: [],
|
|
20
|
+
timestamp: new Date().toISOString(),
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
// ── Mock adapter factory ──
|
|
24
|
+
function mockAdapter(overrides) {
|
|
25
|
+
return {
|
|
26
|
+
name: "test-exchange",
|
|
27
|
+
marketOrder: vi.fn().mockResolvedValue({ orderId: "m1" }),
|
|
28
|
+
limitOrder: vi.fn().mockResolvedValue({ orderId: "l1" }),
|
|
29
|
+
stopOrder: vi.fn().mockResolvedValue({ orderId: "s1" }),
|
|
30
|
+
cancelOrder: vi.fn().mockResolvedValue({ success: true }),
|
|
31
|
+
cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: 3 }),
|
|
32
|
+
editOrder: vi.fn().mockResolvedValue({ success: true }),
|
|
33
|
+
setLeverage: vi.fn().mockResolvedValue({ leverage: 10 }),
|
|
34
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
35
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
36
|
+
getBalance: vi.fn().mockResolvedValue({
|
|
37
|
+
equity: "10000",
|
|
38
|
+
available: "8000",
|
|
39
|
+
marginUsed: "2000",
|
|
40
|
+
unrealizedPnl: "150",
|
|
41
|
+
}),
|
|
42
|
+
getMarkets: vi.fn().mockResolvedValue([]),
|
|
43
|
+
getOrderbook: vi.fn().mockResolvedValue({
|
|
44
|
+
bids: [["42000.50", "1.5"], ["41999.00", "2.0"]],
|
|
45
|
+
asks: [["42001.50", "1.2"], ["42002.00", "3.0"]],
|
|
46
|
+
}),
|
|
47
|
+
getRecentTrades: vi.fn().mockResolvedValue([]),
|
|
48
|
+
getFundingHistory: vi.fn().mockResolvedValue([]),
|
|
49
|
+
getKlines: vi.fn().mockResolvedValue([]),
|
|
50
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
51
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
52
|
+
getFundingPayments: vi.fn().mockResolvedValue([]),
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ── Helpers ──
|
|
57
|
+
function createMarketProgram(adapter, json = false) {
|
|
58
|
+
const program = new Command();
|
|
59
|
+
program.exitOverride();
|
|
60
|
+
program.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
61
|
+
registerMarketCommands(program, async () => adapter, () => json);
|
|
62
|
+
return program;
|
|
63
|
+
}
|
|
64
|
+
function createAccountProgram(adapter, json = false) {
|
|
65
|
+
const program = new Command();
|
|
66
|
+
program.exitOverride();
|
|
67
|
+
program.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
68
|
+
registerAccountCommands(program, async () => adapter, () => json);
|
|
69
|
+
return program;
|
|
70
|
+
}
|
|
71
|
+
function createTradeProgram(adapter, json = false) {
|
|
72
|
+
const program = new Command();
|
|
73
|
+
program.exitOverride();
|
|
74
|
+
program.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
75
|
+
registerTradeCommands(program, async () => adapter, () => json);
|
|
76
|
+
return program;
|
|
77
|
+
}
|
|
78
|
+
async function run(program, args) {
|
|
79
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
80
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
81
|
+
try {
|
|
82
|
+
await program.parseAsync(["node", "perp", ...args]);
|
|
83
|
+
return log.mock.calls;
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
log.mockRestore();
|
|
87
|
+
err.mockRestore();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseJsonOutput(calls) {
|
|
91
|
+
// Find the call that contains JSON output
|
|
92
|
+
for (const call of calls) {
|
|
93
|
+
const str = String(call[0]);
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(str);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// not JSON, continue
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
});
|
|
106
|
+
// ══════════════════════════════════════════════════════════════
|
|
107
|
+
// 1. market mid <symbol>
|
|
108
|
+
// ══════════════════════════════════════════════════════════════
|
|
109
|
+
describe("market mid", () => {
|
|
110
|
+
it("calls getOrderbook with uppercased symbol", async () => {
|
|
111
|
+
const adapter = mockAdapter();
|
|
112
|
+
const program = createMarketProgram(adapter);
|
|
113
|
+
await run(program, ["market", "mid", "btc"]);
|
|
114
|
+
expect(adapter.getOrderbook).toHaveBeenCalledWith("BTC");
|
|
115
|
+
});
|
|
116
|
+
it("returns mid price in JSON mode", async () => {
|
|
117
|
+
const adapter = mockAdapter();
|
|
118
|
+
const program = createMarketProgram(adapter, true);
|
|
119
|
+
const calls = await run(program, ["market", "mid", "eth"]);
|
|
120
|
+
const output = parseJsonOutput(calls);
|
|
121
|
+
expect(output).toMatchObject({
|
|
122
|
+
ok: true,
|
|
123
|
+
data: {
|
|
124
|
+
symbol: "ETH",
|
|
125
|
+
mid: expect.any(String),
|
|
126
|
+
bid: "42000.50",
|
|
127
|
+
ask: "42001.50",
|
|
128
|
+
spread: expect.any(String),
|
|
129
|
+
},
|
|
130
|
+
meta: { timestamp: expect.any(String) },
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
it("calculates correct mid from bid/ask", async () => {
|
|
134
|
+
const adapter = mockAdapter({
|
|
135
|
+
getOrderbook: vi.fn().mockResolvedValue({
|
|
136
|
+
bids: [["100.00", "1"]],
|
|
137
|
+
asks: [["102.00", "1"]],
|
|
138
|
+
}),
|
|
139
|
+
});
|
|
140
|
+
const program = createMarketProgram(adapter, true);
|
|
141
|
+
const calls = await run(program, ["market", "mid", "SOL"]);
|
|
142
|
+
const output = parseJsonOutput(calls);
|
|
143
|
+
expect(output.data.mid).toBe("101");
|
|
144
|
+
// spread = (102-100)/101 * 100 = 1.980198...%
|
|
145
|
+
expect(parseFloat(output.data.spread)).toBeCloseTo(1.9802, 3);
|
|
146
|
+
});
|
|
147
|
+
it("handles empty orderbook gracefully in JSON mode", async () => {
|
|
148
|
+
const adapter = mockAdapter({
|
|
149
|
+
getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
|
|
150
|
+
});
|
|
151
|
+
const program = createMarketProgram(adapter, true);
|
|
152
|
+
const calls = await run(program, ["market", "mid", "BTC"]);
|
|
153
|
+
const output = parseJsonOutput(calls);
|
|
154
|
+
expect(output.ok).toBe(false);
|
|
155
|
+
expect(output.error?.code).toBe("SYMBOL_NOT_FOUND");
|
|
156
|
+
});
|
|
157
|
+
it("handles ask-only orderbook (no bids)", async () => {
|
|
158
|
+
const adapter = mockAdapter({
|
|
159
|
+
getOrderbook: vi.fn().mockResolvedValue({
|
|
160
|
+
bids: [],
|
|
161
|
+
asks: [["50000", "1"]],
|
|
162
|
+
}),
|
|
163
|
+
});
|
|
164
|
+
const program = createMarketProgram(adapter, true);
|
|
165
|
+
const calls = await run(program, ["market", "mid", "BTC"]);
|
|
166
|
+
const output = parseJsonOutput(calls);
|
|
167
|
+
expect(output.ok).toBe(true);
|
|
168
|
+
expect(output.data.mid).toBe("50000");
|
|
169
|
+
expect(output.data.bid).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
it("handles bid-only orderbook (no asks)", async () => {
|
|
172
|
+
const adapter = mockAdapter({
|
|
173
|
+
getOrderbook: vi.fn().mockResolvedValue({
|
|
174
|
+
bids: [["49000", "1"]],
|
|
175
|
+
asks: [],
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
const program = createMarketProgram(adapter, true);
|
|
179
|
+
const calls = await run(program, ["market", "mid", "BTC"]);
|
|
180
|
+
const output = parseJsonOutput(calls);
|
|
181
|
+
expect(output.ok).toBe(true);
|
|
182
|
+
expect(output.data.mid).toBe("49000");
|
|
183
|
+
expect(output.data.ask).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ══════════════════════════════════════════════════════════════
|
|
187
|
+
// 2. account margin <symbol>
|
|
188
|
+
// ══════════════════════════════════════════════════════════════
|
|
189
|
+
describe("account margin", () => {
|
|
190
|
+
const btcPosition = {
|
|
191
|
+
symbol: "BTC",
|
|
192
|
+
side: "long",
|
|
193
|
+
size: "0.5",
|
|
194
|
+
entryPrice: "100000",
|
|
195
|
+
markPrice: "101000",
|
|
196
|
+
liquidationPrice: "90000",
|
|
197
|
+
unrealizedPnl: "500",
|
|
198
|
+
leverage: 10,
|
|
199
|
+
};
|
|
200
|
+
it("returns margin details for an existing position in JSON mode", async () => {
|
|
201
|
+
const adapter = mockAdapter({
|
|
202
|
+
getPositions: vi.fn().mockResolvedValue([btcPosition]),
|
|
203
|
+
});
|
|
204
|
+
const program = createAccountProgram(adapter, true);
|
|
205
|
+
const calls = await run(program, ["account", "margin", "btc"]);
|
|
206
|
+
const output = parseJsonOutput(calls);
|
|
207
|
+
expect(output.ok).toBe(true);
|
|
208
|
+
expect(output.data.symbol).toBe("BTC");
|
|
209
|
+
expect(output.data.side).toBe("long");
|
|
210
|
+
expect(output.data.leverage).toBe(10);
|
|
211
|
+
// notional = 0.5 * 101000 = 50500
|
|
212
|
+
expect(output.data.notional).toBe("50500.00");
|
|
213
|
+
// marginRequired = 50500 / 10 = 5050
|
|
214
|
+
expect(output.data.marginRequired).toBe("5050.00");
|
|
215
|
+
// marginPct = 5050 / 10000 * 100 = 50.5%
|
|
216
|
+
expect(output.data.marginPctOfEquity).toBe("50.50");
|
|
217
|
+
expect(output.data.accountEquity).toBe("10000");
|
|
218
|
+
});
|
|
219
|
+
it("calls both getPositions and getBalance in parallel", async () => {
|
|
220
|
+
const adapter = mockAdapter({
|
|
221
|
+
getPositions: vi.fn().mockResolvedValue([btcPosition]),
|
|
222
|
+
});
|
|
223
|
+
const program = createAccountProgram(adapter, true);
|
|
224
|
+
await run(program, ["account", "margin", "BTC"]);
|
|
225
|
+
expect(adapter.getPositions).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(adapter.getBalance).toHaveBeenCalledTimes(1);
|
|
227
|
+
});
|
|
228
|
+
it("returns POSITION_NOT_FOUND for non-existent position in JSON mode", async () => {
|
|
229
|
+
const adapter = mockAdapter({
|
|
230
|
+
getPositions: vi.fn().mockResolvedValue([]),
|
|
231
|
+
});
|
|
232
|
+
const program = createAccountProgram(adapter, true);
|
|
233
|
+
const calls = await run(program, ["account", "margin", "DOGE"]);
|
|
234
|
+
const output = parseJsonOutput(calls);
|
|
235
|
+
expect(output.ok).toBe(false);
|
|
236
|
+
expect(output.error?.code).toBe("POSITION_NOT_FOUND");
|
|
237
|
+
});
|
|
238
|
+
it("matches symbol case-insensitively and with -PERP suffix", async () => {
|
|
239
|
+
const ethPos = { ...btcPosition, symbol: "ETH-PERP" };
|
|
240
|
+
const adapter = mockAdapter({
|
|
241
|
+
getPositions: vi.fn().mockResolvedValue([ethPos]),
|
|
242
|
+
});
|
|
243
|
+
const program = createAccountProgram(adapter, true);
|
|
244
|
+
const calls = await run(program, ["account", "margin", "eth"]);
|
|
245
|
+
const output = parseJsonOutput(calls);
|
|
246
|
+
expect(output.ok).toBe(true);
|
|
247
|
+
expect(output.data.symbol).toBe("ETH-PERP");
|
|
248
|
+
});
|
|
249
|
+
it("displays text output for non-JSON mode", async () => {
|
|
250
|
+
const adapter = mockAdapter({
|
|
251
|
+
getPositions: vi.fn().mockResolvedValue([btcPosition]),
|
|
252
|
+
});
|
|
253
|
+
const program = createAccountProgram(adapter, false);
|
|
254
|
+
const calls = await run(program, ["account", "margin", "BTC"]);
|
|
255
|
+
const allOutput = calls.map(c => String(c[0])).join("\n");
|
|
256
|
+
expect(allOutput).toContain("BTC");
|
|
257
|
+
expect(allOutput).toContain("Margin");
|
|
258
|
+
});
|
|
259
|
+
it("computes zero margin when leverage is 0", async () => {
|
|
260
|
+
const zeroLev = { ...btcPosition, leverage: 0 };
|
|
261
|
+
const adapter = mockAdapter({
|
|
262
|
+
getPositions: vi.fn().mockResolvedValue([zeroLev]),
|
|
263
|
+
});
|
|
264
|
+
const program = createAccountProgram(adapter, true);
|
|
265
|
+
const calls = await run(program, ["account", "margin", "BTC"]);
|
|
266
|
+
const output = parseJsonOutput(calls);
|
|
267
|
+
expect(output.ok).toBe(true);
|
|
268
|
+
expect(output.data.marginRequired).toBe("0.00");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
// ══════════════════════════════════════════════════════════════
|
|
272
|
+
// 3. trade status <orderId>
|
|
273
|
+
// ══════════════════════════════════════════════════════════════
|
|
274
|
+
describe("trade status", () => {
|
|
275
|
+
it("searches open orders and order history for the orderId", async () => {
|
|
276
|
+
const openOrder = {
|
|
277
|
+
orderId: "5555",
|
|
278
|
+
symbol: "BTC",
|
|
279
|
+
side: "buy",
|
|
280
|
+
price: "99000",
|
|
281
|
+
size: "0.1",
|
|
282
|
+
filled: "0",
|
|
283
|
+
status: "open",
|
|
284
|
+
type: "limit",
|
|
285
|
+
};
|
|
286
|
+
const adapter = mockAdapter({
|
|
287
|
+
getOpenOrders: vi.fn().mockResolvedValue([openOrder]),
|
|
288
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
289
|
+
});
|
|
290
|
+
const program = createTradeProgram(adapter, true);
|
|
291
|
+
const calls = await run(program, ["trade", "status", "5555"]);
|
|
292
|
+
const output = parseJsonOutput(calls);
|
|
293
|
+
expect(output.ok).toBe(true);
|
|
294
|
+
expect(output.data.orderId).toBe("5555");
|
|
295
|
+
expect(output.data.status).toBe("open");
|
|
296
|
+
});
|
|
297
|
+
it("finds order in history if not in open orders", async () => {
|
|
298
|
+
const filledOrder = {
|
|
299
|
+
orderId: "7777",
|
|
300
|
+
symbol: "ETH",
|
|
301
|
+
side: "sell",
|
|
302
|
+
price: "3500",
|
|
303
|
+
size: "1.0",
|
|
304
|
+
filled: "1.0",
|
|
305
|
+
status: "filled",
|
|
306
|
+
type: "market",
|
|
307
|
+
};
|
|
308
|
+
const adapter = mockAdapter({
|
|
309
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
310
|
+
getOrderHistory: vi.fn().mockResolvedValue([filledOrder]),
|
|
311
|
+
});
|
|
312
|
+
const program = createTradeProgram(adapter, true);
|
|
313
|
+
const calls = await run(program, ["trade", "status", "7777"]);
|
|
314
|
+
const output = parseJsonOutput(calls);
|
|
315
|
+
expect(output.ok).toBe(true);
|
|
316
|
+
expect(output.data.orderId).toBe("7777");
|
|
317
|
+
expect(output.data.status).toBe("filled");
|
|
318
|
+
});
|
|
319
|
+
it("returns ORDER_NOT_FOUND when order doesn't exist", async () => {
|
|
320
|
+
const adapter = mockAdapter({
|
|
321
|
+
getOpenOrders: vi.fn().mockResolvedValue([]),
|
|
322
|
+
getOrderHistory: vi.fn().mockResolvedValue([]),
|
|
323
|
+
});
|
|
324
|
+
const program = createTradeProgram(adapter, true);
|
|
325
|
+
const calls = await run(program, ["trade", "status", "9999"]);
|
|
326
|
+
const output = parseJsonOutput(calls);
|
|
327
|
+
expect(output.ok).toBe(false);
|
|
328
|
+
expect(output.error?.code).toBe("ORDER_NOT_FOUND");
|
|
329
|
+
});
|
|
330
|
+
it("displays text output for non-JSON mode when found", async () => {
|
|
331
|
+
const order = {
|
|
332
|
+
orderId: "1234",
|
|
333
|
+
symbol: "SOL",
|
|
334
|
+
side: "buy",
|
|
335
|
+
price: "150",
|
|
336
|
+
size: "10",
|
|
337
|
+
filled: "5",
|
|
338
|
+
status: "open",
|
|
339
|
+
type: "limit",
|
|
340
|
+
};
|
|
341
|
+
const adapter = mockAdapter({
|
|
342
|
+
getOpenOrders: vi.fn().mockResolvedValue([order]),
|
|
343
|
+
});
|
|
344
|
+
const program = createTradeProgram(adapter, false);
|
|
345
|
+
const calls = await run(program, ["trade", "status", "1234"]);
|
|
346
|
+
const allOutput = calls.map(c => String(c[0])).join("\n");
|
|
347
|
+
expect(allOutput).toContain("SOL");
|
|
348
|
+
expect(allOutput).toContain("1234");
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
// ══════════════════════════════════════════════════════════════
|
|
352
|
+
// 4. trade fills [symbol]
|
|
353
|
+
// ══════════════════════════════════════════════════════════════
|
|
354
|
+
describe("trade fills", () => {
|
|
355
|
+
const sampleTrades = [
|
|
356
|
+
{ time: 1700000000000, symbol: "BTC", side: "buy", price: "42000", size: "0.1", fee: "4.20" },
|
|
357
|
+
{ time: 1700000001000, symbol: "ETH", side: "sell", price: "3500", size: "1.0", fee: "3.50" },
|
|
358
|
+
{ time: 1700000002000, symbol: "BTC", side: "sell", price: "42100", size: "0.05", fee: "2.10" },
|
|
359
|
+
];
|
|
360
|
+
it("returns all fills when no symbol filter", async () => {
|
|
361
|
+
const adapter = mockAdapter({
|
|
362
|
+
getTradeHistory: vi.fn().mockResolvedValue(sampleTrades),
|
|
363
|
+
});
|
|
364
|
+
const program = createTradeProgram(adapter, true);
|
|
365
|
+
const calls = await run(program, ["trade", "fills"]);
|
|
366
|
+
const output = parseJsonOutput(calls);
|
|
367
|
+
expect(output.ok).toBe(true);
|
|
368
|
+
expect(output.data).toHaveLength(3);
|
|
369
|
+
});
|
|
370
|
+
it("filters by symbol when provided", async () => {
|
|
371
|
+
const adapter = mockAdapter({
|
|
372
|
+
getTradeHistory: vi.fn().mockResolvedValue(sampleTrades),
|
|
373
|
+
});
|
|
374
|
+
const program = createTradeProgram(adapter, true);
|
|
375
|
+
const calls = await run(program, ["trade", "fills", "BTC"]);
|
|
376
|
+
const output = parseJsonOutput(calls);
|
|
377
|
+
expect(output.ok).toBe(true);
|
|
378
|
+
expect(output.data).toHaveLength(2);
|
|
379
|
+
expect(output.data.every(t => t.symbol === "BTC")).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
it("symbol filter is case-insensitive", async () => {
|
|
382
|
+
const adapter = mockAdapter({
|
|
383
|
+
getTradeHistory: vi.fn().mockResolvedValue(sampleTrades),
|
|
384
|
+
});
|
|
385
|
+
const program = createTradeProgram(adapter, true);
|
|
386
|
+
const calls = await run(program, ["trade", "fills", "eth"]);
|
|
387
|
+
const output = parseJsonOutput(calls);
|
|
388
|
+
expect(output.ok).toBe(true);
|
|
389
|
+
expect(output.data).toHaveLength(1);
|
|
390
|
+
expect(output.data[0].symbol).toBe("ETH");
|
|
391
|
+
});
|
|
392
|
+
it("returns empty array when symbol has no fills", async () => {
|
|
393
|
+
const adapter = mockAdapter({
|
|
394
|
+
getTradeHistory: vi.fn().mockResolvedValue(sampleTrades),
|
|
395
|
+
});
|
|
396
|
+
const program = createTradeProgram(adapter, true);
|
|
397
|
+
const calls = await run(program, ["trade", "fills", "DOGE"]);
|
|
398
|
+
const output = parseJsonOutput(calls);
|
|
399
|
+
expect(output.ok).toBe(true);
|
|
400
|
+
expect(output.data).toHaveLength(0);
|
|
401
|
+
});
|
|
402
|
+
it("respects --limit option", async () => {
|
|
403
|
+
const adapter = mockAdapter({
|
|
404
|
+
getTradeHistory: vi.fn().mockResolvedValue(sampleTrades),
|
|
405
|
+
});
|
|
406
|
+
const program = createTradeProgram(adapter, true);
|
|
407
|
+
await run(program, ["trade", "fills", "--limit", "10"]);
|
|
408
|
+
expect(adapter.getTradeHistory).toHaveBeenCalledWith(10);
|
|
409
|
+
});
|
|
410
|
+
it("passes default limit of 30 to getTradeHistory", async () => {
|
|
411
|
+
const adapter = mockAdapter({
|
|
412
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
413
|
+
});
|
|
414
|
+
const program = createTradeProgram(adapter, true);
|
|
415
|
+
await run(program, ["trade", "fills"]);
|
|
416
|
+
expect(adapter.getTradeHistory).toHaveBeenCalledWith(30);
|
|
417
|
+
});
|
|
418
|
+
it("matches -PERP suffix variants", async () => {
|
|
419
|
+
const perpTrades = [
|
|
420
|
+
{ time: 1700000000000, symbol: "SOL-PERP", side: "buy", price: "150", size: "10", fee: "1.50" },
|
|
421
|
+
];
|
|
422
|
+
const adapter = mockAdapter({
|
|
423
|
+
getTradeHistory: vi.fn().mockResolvedValue(perpTrades),
|
|
424
|
+
});
|
|
425
|
+
const program = createTradeProgram(adapter, true);
|
|
426
|
+
const calls = await run(program, ["trade", "fills", "SOL"]);
|
|
427
|
+
const output = parseJsonOutput(calls);
|
|
428
|
+
expect(output.ok).toBe(true);
|
|
429
|
+
expect(output.data).toHaveLength(1);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
// ══════════════════════════════════════════════════════════════
|
|
433
|
+
// 5. Error handling edge cases (cross-cutting)
|
|
434
|
+
// ══════════════════════════════════════════════════════════════
|
|
435
|
+
describe("error handling edge cases", () => {
|
|
436
|
+
it("market mid handles adapter error gracefully (text mode)", async () => {
|
|
437
|
+
const adapter = mockAdapter({
|
|
438
|
+
getOrderbook: vi.fn().mockRejectedValue(new Error("Connection refused")),
|
|
439
|
+
});
|
|
440
|
+
const program = createMarketProgram(adapter, false);
|
|
441
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => { });
|
|
442
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
443
|
+
try {
|
|
444
|
+
await program.parseAsync(["node", "perp", "market", "mid", "BTC"]);
|
|
445
|
+
const errOutput = err.mock.calls.map(c => String(c[0])).join("\n");
|
|
446
|
+
expect(errOutput).toContain("Connection refused");
|
|
447
|
+
}
|
|
448
|
+
finally {
|
|
449
|
+
log.mockRestore();
|
|
450
|
+
err.mockRestore();
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
it("market mid returns JSON error envelope on adapter failure", async () => {
|
|
454
|
+
const adapter = mockAdapter({
|
|
455
|
+
getOrderbook: vi.fn().mockRejectedValue(new Error("Timeout")),
|
|
456
|
+
});
|
|
457
|
+
const program = createMarketProgram(adapter, true);
|
|
458
|
+
const calls = await run(program, ["market", "mid", "BTC"]);
|
|
459
|
+
const output = parseJsonOutput(calls);
|
|
460
|
+
expect(output.ok).toBe(false);
|
|
461
|
+
expect(output.error?.code).toBeDefined();
|
|
462
|
+
});
|
|
463
|
+
it("account margin handles adapter error gracefully", async () => {
|
|
464
|
+
const adapter = mockAdapter({
|
|
465
|
+
getPositions: vi.fn().mockRejectedValue(new Error("Timeout")),
|
|
466
|
+
});
|
|
467
|
+
const program = createAccountProgram(adapter, true);
|
|
468
|
+
const calls = await run(program, ["account", "margin", "BTC"]);
|
|
469
|
+
const output = parseJsonOutput(calls);
|
|
470
|
+
expect(output.ok).toBe(false);
|
|
471
|
+
expect(output.error?.code).toBeDefined();
|
|
472
|
+
});
|
|
473
|
+
it("trade fills handles empty trade history", async () => {
|
|
474
|
+
const adapter = mockAdapter({
|
|
475
|
+
getTradeHistory: vi.fn().mockResolvedValue([]),
|
|
476
|
+
});
|
|
477
|
+
const program = createTradeProgram(adapter, true);
|
|
478
|
+
const calls = await run(program, ["trade", "fills"]);
|
|
479
|
+
const output = parseJsonOutput(calls);
|
|
480
|
+
expect(output.ok).toBe(true);
|
|
481
|
+
expect(output.data).toHaveLength(0);
|
|
482
|
+
});
|
|
483
|
+
it("JSON envelope always has meta.timestamp", async () => {
|
|
484
|
+
const adapter = mockAdapter();
|
|
485
|
+
const program = createMarketProgram(adapter, true);
|
|
486
|
+
const calls = await run(program, ["market", "mid", "BTC"]);
|
|
487
|
+
const output = parseJsonOutput(calls);
|
|
488
|
+
expect(output.meta?.timestamp).toBeDefined();
|
|
489
|
+
// Should be valid ISO 8601
|
|
490
|
+
expect(new Date(output.meta.timestamp).toISOString()).toBe(output.meta.timestamp);
|
|
491
|
+
});
|
|
492
|
+
it("JSON error envelope has meta.timestamp", async () => {
|
|
493
|
+
const adapter = mockAdapter({
|
|
494
|
+
getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
|
|
495
|
+
});
|
|
496
|
+
const program = createMarketProgram(adapter, true);
|
|
497
|
+
const calls = await run(program, ["market", "mid", "BTC"]);
|
|
498
|
+
const output = parseJsonOutput(calls);
|
|
499
|
+
expect(output.ok).toBe(false);
|
|
500
|
+
expect(output.meta?.timestamp).toBeDefined();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|