perp-cli 0.11.0 → 0.12.0

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.
Files changed (240) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bridge-engine.js +102 -49
  21. package/dist/cli-spec.js +1 -2
  22. package/dist/commands/account.js +2 -500
  23. package/dist/commands/agent.d.ts +8 -2
  24. package/dist/commands/agent.js +1903 -366
  25. package/dist/commands/alerts.js +3 -1
  26. package/dist/commands/arb/index.js +37 -7
  27. package/dist/commands/arb-auto.js +44 -11
  28. package/dist/commands/arb.js +83 -34
  29. package/dist/commands/bot.d.ts +1 -1
  30. package/dist/commands/bot.js +18 -14
  31. package/dist/commands/bridge.d.ts +1 -1
  32. package/dist/commands/bridge.js +4 -4
  33. package/dist/commands/funds.d.ts +1 -1
  34. package/dist/commands/funds.js +18 -68
  35. package/dist/commands/jobs.d.ts +1 -1
  36. package/dist/commands/jobs.js +2 -2
  37. package/dist/commands/manage.d.ts +1 -1
  38. package/dist/commands/manage.js +12 -84
  39. package/dist/commands/plan.d.ts +1 -1
  40. package/dist/commands/plan.js +2 -2
  41. package/dist/commands/portfolio.d.ts +19 -3
  42. package/dist/commands/portfolio.js +747 -163
  43. package/dist/commands/rebalance.d.ts +1 -1
  44. package/dist/commands/rebalance.js +18 -12
  45. package/dist/commands/risk.js +2 -6
  46. package/dist/commands/settings.js +30 -53
  47. package/dist/commands/trade.js +16 -15
  48. package/dist/commands/wallet.d.ts +2 -1
  49. package/dist/commands/wallet.js +756 -28
  50. package/dist/errors.d.ts +64 -0
  51. package/dist/errors.js +82 -2
  52. package/dist/event-stream.js +12 -1
  53. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  54. package/dist/exchanges/aster-typed-data.js +189 -0
  55. package/dist/exchanges/aster.d.ts +60 -11
  56. package/dist/exchanges/aster.js +278 -136
  57. package/dist/exchanges/hyperliquid.d.ts +14 -0
  58. package/dist/exchanges/hyperliquid.js +84 -34
  59. package/dist/exchanges/lighter-spot.js +14 -3
  60. package/dist/exchanges/lighter.d.ts +66 -0
  61. package/dist/exchanges/lighter.js +199 -27
  62. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  63. package/dist/exchanges/pacifica-typed-data.js +74 -0
  64. package/dist/exchanges/pacifica.d.ts +29 -1
  65. package/dist/exchanges/pacifica.js +94 -6
  66. package/dist/funding/rates.js +59 -21
  67. package/dist/guardrail/perp-guardrail.js +9 -1
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +255 -360
  70. package/dist/jobs.js +3 -1
  71. package/dist/mcp-server.js +85 -83
  72. package/dist/position-history.js +47 -4
  73. package/dist/settings.d.ts +67 -3
  74. package/dist/settings.js +1 -2
  75. package/dist/signer/ows-evm.d.ts +14 -0
  76. package/dist/signer/ows-evm.js +28 -9
  77. package/dist/smart-order.d.ts +8 -2
  78. package/dist/smart-order.js +46 -11
  79. package/dist/strategies/funding-arb.js +29 -8
  80. package/dist/trade-validator.js +30 -5
  81. package/dist/utils.d.ts +10 -0
  82. package/dist/utils.js +19 -0
  83. package/package.json +2 -2
  84. package/skills/perp-cli/SKILL.md +23 -14
  85. package/skills/perp-cli/references/agent-operations.md +6 -4
  86. package/skills/perp-cli/references/commands.md +55 -34
  87. package/skills/perp-cli/scripts/preflight.sh +4 -4
  88. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  89. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  90. package/dist/__tests__/alert-logic.test.js +0 -107
  91. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  92. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  93. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  94. package/dist/__tests__/arb-history-stats.test.js +0 -176
  95. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  96. package/dist/__tests__/arb-logic.test.js +0 -84
  97. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  98. package/dist/__tests__/arb-manage.test.js +0 -253
  99. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  100. package/dist/__tests__/arb-new-features.test.js +0 -457
  101. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  102. package/dist/__tests__/arb-sizing.test.js +0 -48
  103. package/dist/__tests__/arb-state.test.d.ts +0 -1
  104. package/dist/__tests__/arb-state.test.js +0 -284
  105. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  106. package/dist/__tests__/arb-userflow.test.js +0 -945
  107. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  108. package/dist/__tests__/arb-utils.test.js +0 -264
  109. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  110. package/dist/__tests__/bot-conditions.test.js +0 -341
  111. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  112. package/dist/__tests__/client-id-tracker.test.js +0 -137
  113. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  114. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  115. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  116. package/dist/__tests__/commands/order-intent.test.js +0 -600
  117. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  118. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  119. package/dist/__tests__/config.test.d.ts +0 -1
  120. package/dist/__tests__/config.test.js +0 -86
  121. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  122. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  123. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  124. package/dist/__tests__/dex-asset-map.test.js +0 -191
  125. package/dist/__tests__/errors.test.d.ts +0 -1
  126. package/dist/__tests__/errors.test.js +0 -110
  127. package/dist/__tests__/event-stream.test.d.ts +0 -1
  128. package/dist/__tests__/event-stream.test.js +0 -276
  129. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  130. package/dist/__tests__/exchanges/interface.test.js +0 -132
  131. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  132. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  133. package/dist/__tests__/execution-log.test.d.ts +0 -1
  134. package/dist/__tests__/execution-log.test.js +0 -106
  135. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  136. package/dist/__tests__/funding-calc.test.js +0 -71
  137. package/dist/__tests__/funding-history.test.d.ts +0 -1
  138. package/dist/__tests__/funding-history.test.js +0 -343
  139. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  140. package/dist/__tests__/funding-rates.test.js +0 -342
  141. package/dist/__tests__/funding.test.d.ts +0 -1
  142. package/dist/__tests__/funding.test.js +0 -173
  143. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  144. package/dist/__tests__/gap-logic.test.js +0 -43
  145. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  146. package/dist/__tests__/hip3-dex.test.js +0 -234
  147. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  148. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  149. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  150. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  151. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  152. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  153. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  154. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  155. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  156. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  157. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  158. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  159. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  160. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  161. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  162. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  163. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  165. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  166. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  167. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  169. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  170. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  171. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  173. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  175. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  177. package/dist/__tests__/liquidity.test.d.ts +0 -1
  178. package/dist/__tests__/liquidity.test.js +0 -225
  179. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  180. package/dist/__tests__/plan-executor.test.js +0 -314
  181. package/dist/__tests__/position-history.test.d.ts +0 -1
  182. package/dist/__tests__/position-history.test.js +0 -367
  183. package/dist/__tests__/retry.test.d.ts +0 -1
  184. package/dist/__tests__/retry.test.js +0 -310
  185. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  186. package/dist/__tests__/risk-assessment.test.js +0 -288
  187. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  188. package/dist/__tests__/security-adversarial.test.js +0 -574
  189. package/dist/__tests__/strategies.test.d.ts +0 -1
  190. package/dist/__tests__/strategies.test.js +0 -539
  191. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  192. package/dist/__tests__/trade-execution.test.js +0 -129
  193. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  194. package/dist/__tests__/trade-validator.test.js +0 -655
  195. package/dist/__tests__/utils.test.d.ts +0 -1
  196. package/dist/__tests__/utils.test.js +0 -76
  197. package/dist/arb/reconcile.d.ts +0 -28
  198. package/dist/arb/reconcile.js +0 -162
  199. package/dist/commands/alert.d.ts +0 -2
  200. package/dist/commands/alert.js +0 -421
  201. package/dist/commands/analytics.d.ts +0 -3
  202. package/dist/commands/analytics.js +0 -311
  203. package/dist/commands/arb-manage.d.ts +0 -5
  204. package/dist/commands/arb-manage.js +0 -5
  205. package/dist/commands/dashboard.d.ts +0 -3
  206. package/dist/commands/dashboard.js +0 -176
  207. package/dist/commands/deposit.d.ts +0 -4
  208. package/dist/commands/deposit.js +0 -573
  209. package/dist/commands/dex.d.ts +0 -3
  210. package/dist/commands/dex.js +0 -120
  211. package/dist/commands/env.d.ts +0 -2
  212. package/dist/commands/env.js +0 -136
  213. package/dist/commands/funding.d.ts +0 -3
  214. package/dist/commands/funding.js +0 -554
  215. package/dist/commands/gap.d.ts +0 -2
  216. package/dist/commands/gap.js +0 -305
  217. package/dist/commands/health.d.ts +0 -2
  218. package/dist/commands/health.js +0 -67
  219. package/dist/commands/multileg.d.ts +0 -16
  220. package/dist/commands/multileg.js +0 -174
  221. package/dist/commands/ows.d.ts +0 -2
  222. package/dist/commands/ows.js +0 -887
  223. package/dist/commands/pnl.d.ts +0 -12
  224. package/dist/commands/pnl.js +0 -243
  225. package/dist/commands/run.d.ts +0 -3
  226. package/dist/commands/run.js +0 -202
  227. package/dist/commands/stream.d.ts +0 -5
  228. package/dist/commands/stream.js +0 -200
  229. package/dist/commands/withdraw.d.ts +0 -3
  230. package/dist/commands/withdraw.js +0 -187
  231. package/dist/exchanges/edgex.d.ts +0 -62
  232. package/dist/exchanges/edgex.js +0 -552
  233. package/dist/exchanges/nado.d.ts +0 -52
  234. package/dist/exchanges/nado.js +0 -223
  235. package/dist/funding/index.d.ts +0 -3
  236. package/dist/funding/index.js +0 -3
  237. package/dist/retry.d.ts +0 -74
  238. package/dist/retry.js +0 -129
  239. package/dist/ws/market-feed.d.ts +0 -74
  240. package/dist/ws/market-feed.js +0 -227
@@ -1,137 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,502 +0,0 @@
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
- });
@@ -1 +0,0 @@
1
- export {};