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