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,821 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import { Command } from "commander";
3
- import { registerTradeCommands } from "../../commands/trade.js";
4
- // ── Mock dependencies to prevent file I/O ──
5
- vi.mock("../../execution-log.js", () => ({
6
- logExecution: vi.fn(),
7
- }));
8
- vi.mock("../../client-id-tracker.js", () => ({
9
- generateClientId: vi.fn(() => "test-id-123"),
10
- logClientId: vi.fn(),
11
- isOrderDuplicate: vi.fn(() => false),
12
- }));
13
- vi.mock("../../trade-validator.js", () => ({
14
- validateTrade: vi.fn().mockResolvedValue({
15
- valid: true,
16
- checks: [],
17
- warnings: [],
18
- timestamp: new Date().toISOString(),
19
- }),
20
- }));
21
- // Import mocked modules so we can control their behavior per-test
22
- import { generateClientId, logClientId, isOrderDuplicate } from "../../client-id-tracker.js";
23
- import { logExecution } from "../../execution-log.js";
24
- // ── Mock adapter factory ──
25
- function mockAdapter(overrides) {
26
- return {
27
- name: "test-exchange",
28
- marketOrder: vi.fn().mockResolvedValue({ orderId: "m1", status: "filled" }),
29
- limitOrder: vi.fn().mockResolvedValue({ orderId: "l1", status: "open" }),
30
- stopOrder: vi.fn().mockResolvedValue({ orderId: "s1" }),
31
- cancelOrder: vi.fn().mockResolvedValue({ success: true }),
32
- cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: 3 }),
33
- editOrder: vi.fn().mockResolvedValue({ success: true }),
34
- setLeverage: vi.fn().mockResolvedValue({ leverage: 10 }),
35
- getPositions: vi.fn().mockResolvedValue([]),
36
- getOpenOrders: vi.fn().mockResolvedValue([]),
37
- getBalance: vi.fn().mockResolvedValue({
38
- equity: "1000",
39
- available: "800",
40
- marginUsed: "200",
41
- unrealizedPnl: "0",
42
- }),
43
- getMarkets: vi.fn().mockResolvedValue([]),
44
- getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
45
- getRecentTrades: vi.fn().mockResolvedValue([]),
46
- getFundingHistory: vi.fn().mockResolvedValue([]),
47
- getKlines: vi.fn().mockResolvedValue([]),
48
- getOrderHistory: vi.fn().mockResolvedValue([]),
49
- getTradeHistory: vi.fn().mockResolvedValue([]),
50
- getFundingPayments: vi.fn().mockResolvedValue([]),
51
- ...overrides,
52
- };
53
- }
54
- // ── Helper: create a program, register commands, and parse ──
55
- function createProgram(adapter) {
56
- const program = new Command();
57
- program.exitOverride(); // Prevent process.exit on parse errors
58
- program.configureOutput({
59
- writeOut: () => { },
60
- writeErr: () => { },
61
- });
62
- registerTradeCommands(program, async () => adapter, () => false);
63
- return program;
64
- }
65
- async function run(adapter, args) {
66
- const program = createProgram(adapter);
67
- // Suppress console output during tests
68
- const log = vi.spyOn(console, "log").mockImplementation(() => { });
69
- const err = vi.spyOn(console, "error").mockImplementation(() => { });
70
- try {
71
- await program.parseAsync(["node", "perp", ...args]);
72
- }
73
- finally {
74
- log.mockRestore();
75
- err.mockRestore();
76
- }
77
- }
78
- // ── Shared setup ──
79
- beforeEach(() => {
80
- vi.clearAllMocks();
81
- // Reset isOrderDuplicate to default: not a duplicate
82
- vi.mocked(isOrderDuplicate).mockReturnValue(false);
83
- });
84
- // ══════════════════════════════════════════════════════════════
85
- // 1. Market Order -- parameter correctness
86
- // ══════════════════════════════════════════════════════════════
87
- describe("trade market -- parameter correctness", () => {
88
- it("calls adapter.marketOrder with uppercased symbol, lowercased side, and string size", async () => {
89
- const adapter = mockAdapter();
90
- await run(adapter, ["trade", "market", "btc", "buy", "0.1"]);
91
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
92
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.1");
93
- });
94
- it("uppercases mixed-case symbol", async () => {
95
- const adapter = mockAdapter();
96
- await run(adapter, ["trade", "market", "Eth", "sell", "2.5"]);
97
- expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "sell", "2.5");
98
- });
99
- it("lowercases the side argument", async () => {
100
- const adapter = mockAdapter();
101
- await run(adapter, ["trade", "market", "sol", "BUY", "5"]);
102
- expect(adapter.marketOrder).toHaveBeenCalledWith("SOL", "buy", "5");
103
- });
104
- it("passes size as a string, not a number", async () => {
105
- const adapter = mockAdapter();
106
- await run(adapter, ["trade", "market", "btc", "buy", "0.001"]);
107
- const args = vi.mocked(adapter.marketOrder).mock.calls[0];
108
- expect(typeof args[2]).toBe("string");
109
- expect(args[2]).toBe("0.001");
110
- });
111
- it("rejects invalid side (exits with error)", async () => {
112
- const adapter = mockAdapter();
113
- // errorAndExit calls process.exit(1), which we can catch via exitOverride or mock
114
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
115
- throw new Error("process.exit");
116
- });
117
- try {
118
- await run(adapter, ["trade", "market", "btc", "long", "1"]);
119
- }
120
- catch {
121
- // Expected
122
- }
123
- expect(adapter.marketOrder).not.toHaveBeenCalled();
124
- exitSpy.mockRestore();
125
- });
126
- });
127
- // ══════════════════════════════════════════════════════════════
128
- // 2. Market Order -- client ID flow
129
- // ══════════════════════════════════════════════════════════════
130
- describe("trade market -- client ID flow", () => {
131
- it("with --auto-id: generates client ID and logs pending then submitted", async () => {
132
- const adapter = mockAdapter();
133
- await run(adapter, ["trade", "market", "btc", "buy", "0.1", "--auto-id"]);
134
- // generateClientId was called
135
- expect(generateClientId).toHaveBeenCalled();
136
- // logClientId called twice: once pending, once submitted
137
- expect(logClientId).toHaveBeenCalledTimes(2);
138
- const firstCall = vi.mocked(logClientId).mock.calls[0][0];
139
- const secondCall = vi.mocked(logClientId).mock.calls[1][0];
140
- expect(firstCall.clientOrderId).toBe("test-id-123");
141
- expect(firstCall.status).toBe("pending");
142
- expect(firstCall.symbol).toBe("BTC");
143
- expect(firstCall.side).toBe("buy");
144
- expect(firstCall.type).toBe("market");
145
- expect(secondCall.clientOrderId).toBe("test-id-123");
146
- expect(secondCall.status).toBe("submitted");
147
- // Adapter still called
148
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
149
- });
150
- it("with --client-id my-id: uses the provided ID", async () => {
151
- const adapter = mockAdapter();
152
- await run(adapter, ["trade", "market", "btc", "buy", "0.1", "--client-id", "my-id"]);
153
- // generateClientId was NOT called
154
- expect(generateClientId).not.toHaveBeenCalled();
155
- // logClientId called with user's ID
156
- expect(logClientId).toHaveBeenCalledTimes(2);
157
- expect(vi.mocked(logClientId).mock.calls[0][0].clientOrderId).toBe("my-id");
158
- expect(vi.mocked(logClientId).mock.calls[1][0].clientOrderId).toBe("my-id");
159
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
160
- });
161
- it("duplicate detection: when isOrderDuplicate returns true, adapter is NOT called", async () => {
162
- vi.mocked(isOrderDuplicate).mockReturnValue(true);
163
- const adapter = mockAdapter();
164
- await run(adapter, ["trade", "market", "btc", "buy", "0.1", "--client-id", "dup-id"]);
165
- expect(adapter.marketOrder).not.toHaveBeenCalled();
166
- // logClientId should NOT be called for duplicates
167
- expect(logClientId).not.toHaveBeenCalled();
168
- });
169
- it("without --auto-id or --client-id: no client ID tracking", async () => {
170
- const adapter = mockAdapter();
171
- await run(adapter, ["trade", "market", "btc", "buy", "0.1"]);
172
- expect(generateClientId).not.toHaveBeenCalled();
173
- expect(logClientId).not.toHaveBeenCalled();
174
- // Adapter still called
175
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
176
- });
177
- });
178
- // ══════════════════════════════════════════════════════════════
179
- // 3. Limit Order -- parameter correctness
180
- // ══════════════════════════════════════════════════════════════
181
- describe("trade limit -- parameter correctness", () => {
182
- it("calls adapter.limitOrder with uppercased symbol, lowercased side, price, size", async () => {
183
- const adapter = mockAdapter();
184
- await run(adapter, ["trade", "limit", "eth", "sell", "2000", "1.5"]);
185
- expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
186
- expect(adapter.limitOrder).toHaveBeenCalledWith("ETH", "sell", "2000", "1.5");
187
- });
188
- it("all four args are strings", async () => {
189
- const adapter = mockAdapter();
190
- await run(adapter, ["trade", "limit", "btc", "buy", "65000", "0.05"]);
191
- const args = vi.mocked(adapter.limitOrder).mock.calls[0];
192
- expect(typeof args[0]).toBe("string"); // symbol
193
- expect(typeof args[1]).toBe("string"); // side
194
- expect(typeof args[2]).toBe("string"); // price
195
- expect(typeof args[3]).toBe("string"); // size
196
- });
197
- it("client ID works the same as market order", async () => {
198
- const adapter = mockAdapter();
199
- await run(adapter, ["trade", "limit", "btc", "buy", "60000", "0.1", "--auto-id"]);
200
- expect(generateClientId).toHaveBeenCalled();
201
- expect(logClientId).toHaveBeenCalledTimes(2);
202
- expect(vi.mocked(logClientId).mock.calls[0][0].type).toBe("limit");
203
- expect(vi.mocked(logClientId).mock.calls[0][0].status).toBe("pending");
204
- expect(vi.mocked(logClientId).mock.calls[1][0].status).toBe("submitted");
205
- expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
206
- });
207
- it("duplicate detection prevents execution for limit orders too", async () => {
208
- vi.mocked(isOrderDuplicate).mockReturnValue(true);
209
- const adapter = mockAdapter();
210
- await run(adapter, ["trade", "limit", "btc", "buy", "60000", "0.1", "--client-id", "dup"]);
211
- expect(adapter.limitOrder).not.toHaveBeenCalled();
212
- });
213
- });
214
- // ══════════════════════════════════════════════════════════════
215
- // 4. Stop Order -- parameter correctness
216
- // ══════════════════════════════════════════════════════════════
217
- describe("trade stop -- parameter correctness", () => {
218
- it("calls adapter.stopOrder with correct args and options", async () => {
219
- const adapter = mockAdapter();
220
- await run(adapter, [
221
- "trade", "stop", "btc", "sell", "60000", "0.1",
222
- "--limit-price", "59500", "--reduce-only",
223
- ]);
224
- expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
225
- expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.1", "60000", { limitPrice: "59500", reduceOnly: true });
226
- });
227
- it("without --limit-price and --reduce-only: options have undefined values", async () => {
228
- const adapter = mockAdapter();
229
- await run(adapter, ["trade", "stop", "btc", "sell", "60000", "0.1"]);
230
- expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
231
- expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.1", "60000", { limitPrice: undefined, reduceOnly: undefined });
232
- });
233
- it("uppercases symbol for stop orders", async () => {
234
- const adapter = mockAdapter();
235
- await run(adapter, ["trade", "stop", "eth", "buy", "3500", "1"]);
236
- expect(adapter.stopOrder).toHaveBeenCalledWith("ETH", "buy", "1", "3500", { limitPrice: undefined, reduceOnly: undefined });
237
- });
238
- });
239
- // ══════════════════════════════════════════════════════════════
240
- // 5. Cancel Order
241
- // ══════════════════════════════════════════════════════════════
242
- describe("trade cancel -- parameter correctness", () => {
243
- it("calls adapter.cancelOrder with uppercased symbol and orderId", async () => {
244
- const adapter = mockAdapter();
245
- await run(adapter, ["trade", "cancel", "btc", "abc123"]);
246
- expect(adapter.cancelOrder).toHaveBeenCalledTimes(1);
247
- expect(adapter.cancelOrder).toHaveBeenCalledWith("BTC", "abc123");
248
- });
249
- it("uppercases mixed-case symbol", async () => {
250
- const adapter = mockAdapter();
251
- await run(adapter, ["trade", "cancel", "Sol", "order-456"]);
252
- expect(adapter.cancelOrder).toHaveBeenCalledWith("SOL", "order-456");
253
- });
254
- it("no other adapter methods are called", async () => {
255
- const adapter = mockAdapter();
256
- await run(adapter, ["trade", "cancel", "btc", "abc123"]);
257
- expect(adapter.marketOrder).not.toHaveBeenCalled();
258
- expect(adapter.limitOrder).not.toHaveBeenCalled();
259
- expect(adapter.cancelAllOrders).not.toHaveBeenCalled();
260
- });
261
- });
262
- // ══════════════════════════════════════════════════════════════
263
- // 6. Cancel All Orders
264
- // ══════════════════════════════════════════════════════════════
265
- describe("trade cancel-all", () => {
266
- it("calls adapter.cancelAllOrders with no arguments", async () => {
267
- const adapter = mockAdapter();
268
- await run(adapter, ["trade", "cancel-all"]);
269
- expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
270
- expect(adapter.cancelAllOrders).toHaveBeenCalledWith();
271
- });
272
- it("no other adapter methods are called", async () => {
273
- const adapter = mockAdapter();
274
- await run(adapter, ["trade", "cancel-all"]);
275
- expect(adapter.marketOrder).not.toHaveBeenCalled();
276
- expect(adapter.cancelOrder).not.toHaveBeenCalled();
277
- });
278
- });
279
- // ══════════════════════════════════════════════════════════════
280
- // 7. Set Leverage
281
- // ══════════════════════════════════════════════════════════════
282
- describe("trade leverage", () => {
283
- it("calls adapter.setLeverage with uppercased symbol, parsed int, and 'cross' by default", async () => {
284
- const adapter = mockAdapter();
285
- await run(adapter, ["trade", "leverage", "btc", "10"]);
286
- expect(adapter.setLeverage).toHaveBeenCalledTimes(1);
287
- expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "cross");
288
- });
289
- it("with --isolated: passes 'isolated' as margin mode", async () => {
290
- const adapter = mockAdapter();
291
- await run(adapter, ["trade", "leverage", "btc", "10", "--isolated"]);
292
- expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "isolated");
293
- });
294
- it("leverage is parsed as integer", async () => {
295
- const adapter = mockAdapter();
296
- await run(adapter, ["trade", "leverage", "eth", "25"]);
297
- const args = vi.mocked(adapter.setLeverage).mock.calls[0];
298
- expect(args[1]).toBe(25);
299
- expect(typeof args[1]).toBe("number");
300
- });
301
- });
302
- // ══════════════════════════════════════════════════════════════
303
- // 8. Close Position -- side mapping
304
- // ══════════════════════════════════════════════════════════════
305
- describe("trade close -- side mapping", () => {
306
- it("LONG position -> calls marketOrder with 'sell' (opposite side)", async () => {
307
- const adapter = mockAdapter({
308
- getPositions: vi.fn().mockResolvedValue([
309
- {
310
- symbol: "BTC",
311
- side: "long",
312
- size: "0.5",
313
- entryPrice: "60000",
314
- markPrice: "61000",
315
- liquidationPrice: "50000",
316
- unrealizedPnl: "500",
317
- leverage: 10,
318
- },
319
- ]),
320
- });
321
- await run(adapter, ["trade", "close", "btc"]);
322
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
323
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
324
- });
325
- it("SHORT position -> calls marketOrder with 'buy' (opposite side)", async () => {
326
- const adapter = mockAdapter({
327
- getPositions: vi.fn().mockResolvedValue([
328
- {
329
- symbol: "ETH",
330
- side: "short",
331
- size: "3.0",
332
- entryPrice: "3000",
333
- markPrice: "2900",
334
- liquidationPrice: "4000",
335
- unrealizedPnl: "300",
336
- leverage: 5,
337
- },
338
- ]),
339
- });
340
- await run(adapter, ["trade", "close", "eth"]);
341
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
342
- expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "buy", "3.0");
343
- });
344
- it("no position -> error, adapter.marketOrder NOT called", async () => {
345
- const adapter = mockAdapter({
346
- getPositions: vi.fn().mockResolvedValue([]),
347
- });
348
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
349
- throw new Error("process.exit");
350
- });
351
- try {
352
- await run(adapter, ["trade", "close", "btc"]);
353
- }
354
- catch {
355
- // Expected
356
- }
357
- expect(adapter.marketOrder).not.toHaveBeenCalled();
358
- exitSpy.mockRestore();
359
- });
360
- it("uses the full position size to close", async () => {
361
- const adapter = mockAdapter({
362
- getPositions: vi.fn().mockResolvedValue([
363
- {
364
- symbol: "SOL",
365
- side: "long",
366
- size: "100",
367
- entryPrice: "150",
368
- markPrice: "155",
369
- liquidationPrice: "120",
370
- unrealizedPnl: "500",
371
- leverage: 3,
372
- },
373
- ]),
374
- });
375
- await run(adapter, ["trade", "close", "sol"]);
376
- expect(adapter.marketOrder).toHaveBeenCalledWith("SOL", "sell", "100");
377
- });
378
- it("logs execution after closing", async () => {
379
- const adapter = mockAdapter({
380
- getPositions: vi.fn().mockResolvedValue([
381
- {
382
- symbol: "BTC",
383
- side: "long",
384
- size: "0.1",
385
- entryPrice: "60000",
386
- markPrice: "61000",
387
- liquidationPrice: "50000",
388
- unrealizedPnl: "100",
389
- leverage: 10,
390
- },
391
- ]),
392
- });
393
- await run(adapter, ["trade", "close", "btc"]);
394
- expect(logExecution).toHaveBeenCalledTimes(1);
395
- expect(logExecution).toHaveBeenCalledWith(expect.objectContaining({
396
- type: "market_order",
397
- symbol: "BTC",
398
- side: "sell",
399
- size: "0.1",
400
- meta: expect.objectContaining({ action: "close", originalSide: "long" }),
401
- }));
402
- });
403
- });
404
- // ══════════════════════════════════════════════════════════════
405
- // 9. Close All -- iterates all positions
406
- // ══════════════════════════════════════════════════════════════
407
- describe("trade close-all -- iterates all positions", () => {
408
- it("3 positions (BTC long, ETH short, SOL long) -> 3 marketOrder calls with correct opposite sides", async () => {
409
- const adapter = mockAdapter({
410
- getPositions: vi.fn().mockResolvedValue([
411
- { symbol: "BTC", side: "long", size: "0.5", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "500", leverage: 10 },
412
- { symbol: "ETH", side: "short", size: "5.0", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "500", leverage: 5 },
413
- { symbol: "SOL", side: "long", size: "100", entryPrice: "150", markPrice: "155", liquidationPrice: "120", unrealizedPnl: "500", leverage: 3 },
414
- ]),
415
- });
416
- await run(adapter, ["trade", "close-all"]);
417
- expect(adapter.marketOrder).toHaveBeenCalledTimes(3);
418
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "sell", "0.5");
419
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "5.0");
420
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(3, "SOL", "sell", "100");
421
- });
422
- it("0 positions -> no marketOrder calls", async () => {
423
- const adapter = mockAdapter({
424
- getPositions: vi.fn().mockResolvedValue([]),
425
- });
426
- await run(adapter, ["trade", "close-all"]);
427
- expect(adapter.marketOrder).not.toHaveBeenCalled();
428
- });
429
- it("logs execution for each closed position", async () => {
430
- const adapter = mockAdapter({
431
- getPositions: vi.fn().mockResolvedValue([
432
- { symbol: "BTC", side: "long", size: "0.5", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "500", leverage: 10 },
433
- { symbol: "ETH", side: "short", size: "2.0", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "200", leverage: 5 },
434
- ]),
435
- });
436
- await run(adapter, ["trade", "close-all"]);
437
- expect(logExecution).toHaveBeenCalledTimes(2);
438
- expect(logExecution).toHaveBeenNthCalledWith(1, expect.objectContaining({
439
- symbol: "BTC",
440
- side: "sell",
441
- meta: expect.objectContaining({ action: "close-all", originalSide: "long" }),
442
- }));
443
- expect(logExecution).toHaveBeenNthCalledWith(2, expect.objectContaining({
444
- symbol: "ETH",
445
- side: "buy",
446
- meta: expect.objectContaining({ action: "close-all", originalSide: "short" }),
447
- }));
448
- });
449
- });
450
- // ══════════════════════════════════════════════════════════════
451
- // 10. Flatten -- cancel + close
452
- // ══════════════════════════════════════════════════════════════
453
- describe("trade flatten -- cancel + close", () => {
454
- it("calls cancelAllOrders first, then marketOrder for each position with opposite side", async () => {
455
- const callOrder = [];
456
- const adapter = mockAdapter({
457
- cancelAllOrders: vi.fn().mockImplementation(async () => {
458
- callOrder.push("cancelAllOrders");
459
- return { cancelled: 2 };
460
- }),
461
- getPositions: vi.fn().mockResolvedValue([
462
- { symbol: "BTC", side: "long", size: "1.0", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "1000", leverage: 10 },
463
- { symbol: "ETH", side: "short", size: "10", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "1000", leverage: 5 },
464
- ]),
465
- marketOrder: vi.fn().mockImplementation(async (symbol) => {
466
- callOrder.push(`marketOrder:${symbol}`);
467
- return { orderId: "flat" };
468
- }),
469
- });
470
- await run(adapter, ["trade", "flatten"]);
471
- // Verify order: cancel first, then close positions
472
- expect(callOrder).toEqual(["cancelAllOrders", "marketOrder:BTC", "marketOrder:ETH"]);
473
- expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
474
- expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
475
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "sell", "1.0");
476
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "10");
477
- });
478
- it("with no positions: still calls cancelAllOrders, but no marketOrder", async () => {
479
- const adapter = mockAdapter({
480
- getPositions: vi.fn().mockResolvedValue([]),
481
- });
482
- await run(adapter, ["trade", "flatten"]);
483
- expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
484
- expect(adapter.marketOrder).not.toHaveBeenCalled();
485
- });
486
- it("logs execution for each position closed during flatten", async () => {
487
- const adapter = mockAdapter({
488
- getPositions: vi.fn().mockResolvedValue([
489
- { symbol: "SOL", side: "long", size: "50", entryPrice: "150", markPrice: "155", liquidationPrice: "120", unrealizedPnl: "250", leverage: 3 },
490
- ]),
491
- });
492
- await run(adapter, ["trade", "flatten"]);
493
- expect(logExecution).toHaveBeenCalledWith(expect.objectContaining({
494
- type: "market_order",
495
- symbol: "SOL",
496
- side: "sell",
497
- size: "50",
498
- meta: expect.objectContaining({ action: "flatten", originalSide: "long" }),
499
- }));
500
- });
501
- });
502
- // ══════════════════════════════════════════════════════════════
503
- // 11. Reduce -- percentage calculation
504
- // ══════════════════════════════════════════════════════════════
505
- describe("trade reduce -- percentage calculation", () => {
506
- it("BTC long size=10, reduce 50% -> marketOrder('BTC', 'sell', '5')", async () => {
507
- const adapter = mockAdapter({
508
- getPositions: vi.fn().mockResolvedValue([
509
- { symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
510
- ]),
511
- });
512
- await run(adapter, ["trade", "reduce", "btc", "50"]);
513
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
514
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "5");
515
- });
516
- it("BTC short size=4, reduce 25% -> marketOrder('BTC', 'buy', '1')", async () => {
517
- const adapter = mockAdapter({
518
- getPositions: vi.fn().mockResolvedValue([
519
- { symbol: "BTC", side: "short", size: "4", entryPrice: "60000", markPrice: "59000", liquidationPrice: "70000", unrealizedPnl: "4000", leverage: 10 },
520
- ]),
521
- });
522
- await run(adapter, ["trade", "reduce", "btc", "25"]);
523
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
524
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "1");
525
- });
526
- it("reduce 100% -> closes full position", async () => {
527
- const adapter = mockAdapter({
528
- getPositions: vi.fn().mockResolvedValue([
529
- { symbol: "ETH", side: "long", size: "5", entryPrice: "3000", markPrice: "3100", liquidationPrice: "2500", unrealizedPnl: "500", leverage: 5 },
530
- ]),
531
- });
532
- await run(adapter, ["trade", "reduce", "eth", "100"]);
533
- expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "sell", "5");
534
- });
535
- it("no position -> error, no adapter call", async () => {
536
- const adapter = mockAdapter({
537
- getPositions: vi.fn().mockResolvedValue([]),
538
- });
539
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
540
- throw new Error("process.exit");
541
- });
542
- try {
543
- await run(adapter, ["trade", "reduce", "btc", "50"]);
544
- }
545
- catch {
546
- // Expected
547
- }
548
- expect(adapter.marketOrder).not.toHaveBeenCalled();
549
- exitSpy.mockRestore();
550
- });
551
- it("invalid percent 0 -> error, no adapter call", async () => {
552
- const adapter = mockAdapter({
553
- getPositions: vi.fn().mockResolvedValue([
554
- { symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
555
- ]),
556
- });
557
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
558
- throw new Error("process.exit");
559
- });
560
- try {
561
- await run(adapter, ["trade", "reduce", "btc", "0"]);
562
- }
563
- catch {
564
- // Expected
565
- }
566
- expect(adapter.marketOrder).not.toHaveBeenCalled();
567
- exitSpy.mockRestore();
568
- });
569
- it("invalid percent 101 -> error, no adapter call", async () => {
570
- const adapter = mockAdapter({
571
- getPositions: vi.fn().mockResolvedValue([
572
- { symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
573
- ]),
574
- });
575
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
576
- throw new Error("process.exit");
577
- });
578
- try {
579
- await run(adapter, ["trade", "reduce", "btc", "101"]);
580
- }
581
- catch {
582
- // Expected
583
- }
584
- expect(adapter.marketOrder).not.toHaveBeenCalled();
585
- exitSpy.mockRestore();
586
- });
587
- it("invalid percent -1 -> error, no adapter call", async () => {
588
- const adapter = mockAdapter({
589
- getPositions: vi.fn().mockResolvedValue([
590
- { symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
591
- ]),
592
- });
593
- const exitSpy = vi.spyOn(process, "exit").mockImplementation(() => {
594
- throw new Error("process.exit");
595
- });
596
- try {
597
- await run(adapter, ["trade", "reduce", "btc", "-1"]);
598
- }
599
- catch {
600
- // Expected
601
- }
602
- expect(adapter.marketOrder).not.toHaveBeenCalled();
603
- exitSpy.mockRestore();
604
- });
605
- it("logs execution with reduce metadata", async () => {
606
- const adapter = mockAdapter({
607
- getPositions: vi.fn().mockResolvedValue([
608
- { symbol: "BTC", side: "long", size: "10", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "10000", leverage: 10 },
609
- ]),
610
- });
611
- await run(adapter, ["trade", "reduce", "btc", "50"]);
612
- expect(logExecution).toHaveBeenCalledWith(expect.objectContaining({
613
- type: "market_order",
614
- symbol: "BTC",
615
- side: "sell",
616
- size: "5",
617
- meta: expect.objectContaining({ action: "reduce", percent: 50, originalSize: "10", originalSide: "long" }),
618
- }));
619
- });
620
- it("fractional percentage: reduce 33.3% of size 9 -> '2.9970000000000003' (floating point)", async () => {
621
- const adapter = mockAdapter({
622
- getPositions: vi.fn().mockResolvedValue([
623
- { symbol: "BTC", side: "long", size: "9", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "9000", leverage: 10 },
624
- ]),
625
- });
626
- await run(adapter, ["trade", "reduce", "btc", "33.3"]);
627
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
628
- // 9 * 33.3 / 100 = 2.997
629
- const callArgs = vi.mocked(adapter.marketOrder).mock.calls[0];
630
- expect(callArgs[0]).toBe("BTC");
631
- expect(callArgs[1]).toBe("sell");
632
- expect(parseFloat(callArgs[2])).toBeCloseTo(2.997, 2);
633
- });
634
- });
635
- // ══════════════════════════════════════════════════════════════
636
- // 12. Edit Order
637
- // ══════════════════════════════════════════════════════════════
638
- describe("trade edit", () => {
639
- it("calls adapter.editOrder with uppercased symbol, orderId, price, size", async () => {
640
- const adapter = mockAdapter();
641
- await run(adapter, ["trade", "edit", "btc", "order123", "65000", "0.2"]);
642
- expect(adapter.editOrder).toHaveBeenCalledTimes(1);
643
- expect(adapter.editOrder).toHaveBeenCalledWith("BTC", "order123", "65000", "0.2");
644
- });
645
- it("preserves orderId exactly as provided (case-sensitive)", async () => {
646
- const adapter = mockAdapter();
647
- await run(adapter, ["trade", "edit", "eth", "MyOrder-ABC", "3000", "1"]);
648
- expect(adapter.editOrder).toHaveBeenCalledWith("ETH", "MyOrder-ABC", "3000", "1");
649
- });
650
- it("no other adapter methods are called", async () => {
651
- const adapter = mockAdapter();
652
- await run(adapter, ["trade", "edit", "btc", "order123", "65000", "0.2"]);
653
- expect(adapter.marketOrder).not.toHaveBeenCalled();
654
- expect(adapter.limitOrder).not.toHaveBeenCalled();
655
- expect(adapter.cancelOrder).not.toHaveBeenCalled();
656
- });
657
- });
658
- // ══════════════════════════════════════════════════════════════
659
- // 13. TP/SL side mapping verification (via stopOrder on generic adapter)
660
- // ══════════════════════════════════════════════════════════════
661
- describe("trade tpsl -- side mapping for generic (Lighter-style) adapter", () => {
662
- // Note: The tpsl command uses instanceof checks (PacificaAdapter, HyperliquidAdapter, LighterAdapter).
663
- // Our mock adapter won't match any instanceof check, so it will hit the `else` branch
664
- // and call errorAndExit. However, we CAN test the side mapping logic indirectly
665
- // through the close/reduce commands, which is already tested above.
666
- //
667
- // For completeness, we verify the side mapping expectation documented in the spec:
668
- // - BUY position (long): close side = "sell"
669
- // - SELL position (short): close side = "buy"
670
- it("LONG position close side is 'sell'", async () => {
671
- const adapter = mockAdapter({
672
- getPositions: vi.fn().mockResolvedValue([
673
- { symbol: "BTC", side: "long", size: "1.0", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "1000", leverage: 10 },
674
- ]),
675
- });
676
- await run(adapter, ["trade", "close", "btc"]);
677
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "1.0");
678
- });
679
- it("SHORT position close side is 'buy'", async () => {
680
- const adapter = mockAdapter({
681
- getPositions: vi.fn().mockResolvedValue([
682
- { symbol: "BTC", side: "short", size: "1.0", entryPrice: "60000", markPrice: "59000", liquidationPrice: "70000", unrealizedPnl: "1000", leverage: 10 },
683
- ]),
684
- });
685
- await run(adapter, ["trade", "close", "btc"]);
686
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "1.0");
687
- });
688
- it("close-all maps sides correctly for mixed positions", async () => {
689
- const adapter = mockAdapter({
690
- getPositions: vi.fn().mockResolvedValue([
691
- { symbol: "BTC", side: "long", size: "0.5", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "500", leverage: 10 },
692
- { symbol: "ETH", side: "short", size: "5.0", entryPrice: "3000", markPrice: "2900", liquidationPrice: "4000", unrealizedPnl: "500", leverage: 5 },
693
- ]),
694
- });
695
- await run(adapter, ["trade", "close-all"]);
696
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "sell", "0.5");
697
- expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "5.0");
698
- });
699
- it("reduce on LONG uses 'sell'", async () => {
700
- const adapter = mockAdapter({
701
- getPositions: vi.fn().mockResolvedValue([
702
- { symbol: "BTC", side: "long", size: "2", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "2000", leverage: 10 },
703
- ]),
704
- });
705
- await run(adapter, ["trade", "reduce", "btc", "50"]);
706
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "1");
707
- });
708
- it("reduce on SHORT uses 'buy'", async () => {
709
- const adapter = mockAdapter({
710
- getPositions: vi.fn().mockResolvedValue([
711
- { symbol: "BTC", side: "short", size: "2", entryPrice: "60000", markPrice: "59000", liquidationPrice: "70000", unrealizedPnl: "2000", leverage: 10 },
712
- ]),
713
- });
714
- await run(adapter, ["trade", "reduce", "btc", "50"]);
715
- expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "1");
716
- });
717
- });
718
- // ══════════════════════════════════════════════════════════════
719
- // 14. Symbol uppercasing -- cross-cutting concern
720
- // ══════════════════════════════════════════════════════════════
721
- describe("symbol uppercasing -- cross-cutting", () => {
722
- it("market order uppercases", async () => {
723
- const adapter = mockAdapter();
724
- await run(adapter, ["trade", "market", "doge", "buy", "100"]);
725
- expect(vi.mocked(adapter.marketOrder).mock.calls[0][0]).toBe("DOGE");
726
- });
727
- it("limit order uppercases", async () => {
728
- const adapter = mockAdapter();
729
- await run(adapter, ["trade", "limit", "arb", "sell", "1.5", "50"]);
730
- expect(vi.mocked(adapter.limitOrder).mock.calls[0][0]).toBe("ARB");
731
- });
732
- it("stop order uppercases", async () => {
733
- const adapter = mockAdapter();
734
- await run(adapter, ["trade", "stop", "matic", "buy", "1.0", "100"]);
735
- expect(vi.mocked(adapter.stopOrder).mock.calls[0][0]).toBe("MATIC");
736
- });
737
- it("cancel order uppercases", async () => {
738
- const adapter = mockAdapter();
739
- await run(adapter, ["trade", "cancel", "avax", "id1"]);
740
- expect(vi.mocked(adapter.cancelOrder).mock.calls[0][0]).toBe("AVAX");
741
- });
742
- it("edit order uppercases", async () => {
743
- const adapter = mockAdapter();
744
- await run(adapter, ["trade", "edit", "link", "o1", "20", "5"]);
745
- expect(vi.mocked(adapter.editOrder).mock.calls[0][0]).toBe("LINK");
746
- });
747
- it("leverage uppercases", async () => {
748
- const adapter = mockAdapter();
749
- await run(adapter, ["trade", "leverage", "near", "5"]);
750
- expect(vi.mocked(adapter.setLeverage).mock.calls[0][0]).toBe("NEAR");
751
- });
752
- it("close uppercases", async () => {
753
- const adapter = mockAdapter({
754
- getPositions: vi.fn().mockResolvedValue([
755
- { symbol: "APT", side: "long", size: "10", entryPrice: "10", markPrice: "11", liquidationPrice: "8", unrealizedPnl: "10", leverage: 5 },
756
- ]),
757
- });
758
- await run(adapter, ["trade", "close", "apt"]);
759
- expect(vi.mocked(adapter.marketOrder).mock.calls[0][0]).toBe("APT");
760
- });
761
- it("reduce uppercases", async () => {
762
- const adapter = mockAdapter({
763
- getPositions: vi.fn().mockResolvedValue([
764
- { symbol: "OP", side: "short", size: "20", entryPrice: "2", markPrice: "1.9", liquidationPrice: "3", unrealizedPnl: "2", leverage: 3 },
765
- ]),
766
- });
767
- await run(adapter, ["trade", "reduce", "op", "50"]);
768
- expect(vi.mocked(adapter.marketOrder).mock.calls[0][0]).toBe("OP");
769
- });
770
- });
771
- // ══════════════════════════════════════════════════════════════
772
- // 15. No unexpected extra adapter calls
773
- // ══════════════════════════════════════════════════════════════
774
- describe("no unexpected extra adapter calls", () => {
775
- it("market order only calls marketOrder", async () => {
776
- const adapter = mockAdapter();
777
- await run(adapter, ["trade", "market", "btc", "buy", "0.1"]);
778
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
779
- expect(adapter.limitOrder).not.toHaveBeenCalled();
780
- expect(adapter.stopOrder).not.toHaveBeenCalled();
781
- expect(adapter.cancelOrder).not.toHaveBeenCalled();
782
- expect(adapter.cancelAllOrders).not.toHaveBeenCalled();
783
- expect(adapter.editOrder).not.toHaveBeenCalled();
784
- expect(adapter.setLeverage).not.toHaveBeenCalled();
785
- expect(adapter.getPositions).not.toHaveBeenCalled();
786
- });
787
- it("limit order only calls limitOrder", async () => {
788
- const adapter = mockAdapter();
789
- await run(adapter, ["trade", "limit", "btc", "buy", "60000", "0.1"]);
790
- expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
791
- expect(adapter.marketOrder).not.toHaveBeenCalled();
792
- expect(adapter.stopOrder).not.toHaveBeenCalled();
793
- });
794
- it("stop order only calls stopOrder", async () => {
795
- const adapter = mockAdapter();
796
- await run(adapter, ["trade", "stop", "btc", "sell", "60000", "0.1"]);
797
- expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
798
- expect(adapter.marketOrder).not.toHaveBeenCalled();
799
- expect(adapter.limitOrder).not.toHaveBeenCalled();
800
- });
801
- it("leverage only calls setLeverage", async () => {
802
- const adapter = mockAdapter();
803
- await run(adapter, ["trade", "leverage", "btc", "10"]);
804
- expect(adapter.setLeverage).toHaveBeenCalledTimes(1);
805
- expect(adapter.marketOrder).not.toHaveBeenCalled();
806
- });
807
- it("flatten calls cancelAllOrders + getPositions + marketOrder only", async () => {
808
- const adapter = mockAdapter({
809
- getPositions: vi.fn().mockResolvedValue([
810
- { symbol: "BTC", side: "long", size: "1", entryPrice: "60000", markPrice: "61000", liquidationPrice: "50000", unrealizedPnl: "1000", leverage: 10 },
811
- ]),
812
- });
813
- await run(adapter, ["trade", "flatten"]);
814
- expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
815
- expect(adapter.getPositions).toHaveBeenCalledTimes(1);
816
- expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
817
- expect(adapter.limitOrder).not.toHaveBeenCalled();
818
- expect(adapter.stopOrder).not.toHaveBeenCalled();
819
- expect(adapter.editOrder).not.toHaveBeenCalled();
820
- });
821
- });