perp-cli 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (325) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/__tests__/alert-logic.test.d.ts +1 -0
  4. package/dist/__tests__/alert-logic.test.js +107 -0
  5. package/dist/__tests__/arb-auto-3dex.test.d.ts +1 -0
  6. package/dist/__tests__/arb-auto-3dex.test.js +397 -0
  7. package/dist/__tests__/arb-history-stats.test.d.ts +1 -0
  8. package/dist/__tests__/arb-history-stats.test.js +176 -0
  9. package/dist/__tests__/arb-logic.test.d.ts +1 -0
  10. package/dist/__tests__/arb-logic.test.js +84 -0
  11. package/dist/__tests__/arb-manage.test.d.ts +1 -0
  12. package/dist/__tests__/arb-manage.test.js +253 -0
  13. package/dist/__tests__/arb-new-features.test.d.ts +1 -0
  14. package/dist/__tests__/arb-new-features.test.js +457 -0
  15. package/dist/__tests__/arb-sizing.test.d.ts +1 -0
  16. package/dist/__tests__/arb-sizing.test.js +48 -0
  17. package/dist/__tests__/arb-state.test.d.ts +1 -0
  18. package/dist/__tests__/arb-state.test.js +284 -0
  19. package/dist/__tests__/arb-userflow.test.d.ts +1 -0
  20. package/dist/__tests__/arb-userflow.test.js +945 -0
  21. package/dist/__tests__/arb-utils.test.d.ts +1 -0
  22. package/dist/__tests__/arb-utils.test.js +264 -0
  23. package/dist/__tests__/bot-conditions.test.d.ts +1 -0
  24. package/dist/__tests__/bot-conditions.test.js +341 -0
  25. package/dist/__tests__/client-id-tracker.test.d.ts +1 -0
  26. package/dist/__tests__/client-id-tracker.test.js +137 -0
  27. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +1 -0
  28. package/dist/__tests__/commands/new-atomic-commands.test.js +502 -0
  29. package/dist/__tests__/commands/order-intent.test.d.ts +1 -0
  30. package/dist/__tests__/commands/order-intent.test.js +600 -0
  31. package/dist/__tests__/commands/trade-commands.test.d.ts +1 -0
  32. package/dist/__tests__/commands/trade-commands.test.js +821 -0
  33. package/dist/__tests__/config.test.d.ts +1 -0
  34. package/dist/__tests__/config.test.js +86 -0
  35. package/dist/__tests__/cross-chain-margin.test.d.ts +1 -0
  36. package/dist/__tests__/cross-chain-margin.test.js +287 -0
  37. package/dist/__tests__/dex-asset-map.test.d.ts +1 -0
  38. package/dist/__tests__/dex-asset-map.test.js +191 -0
  39. package/dist/__tests__/errors.test.d.ts +1 -0
  40. package/dist/__tests__/errors.test.js +110 -0
  41. package/dist/__tests__/event-stream.test.d.ts +1 -0
  42. package/dist/__tests__/event-stream.test.js +276 -0
  43. package/dist/__tests__/exchanges/interface.test.d.ts +1 -0
  44. package/dist/__tests__/exchanges/interface.test.js +132 -0
  45. package/dist/__tests__/exchanges/mock-adapter.d.ts +69 -0
  46. package/dist/__tests__/exchanges/mock-adapter.js +137 -0
  47. package/dist/__tests__/execution-log.test.d.ts +1 -0
  48. package/dist/__tests__/execution-log.test.js +106 -0
  49. package/dist/__tests__/funding-calc.test.d.ts +1 -0
  50. package/dist/__tests__/funding-calc.test.js +71 -0
  51. package/dist/__tests__/funding-history.test.d.ts +1 -0
  52. package/dist/__tests__/funding-history.test.js +343 -0
  53. package/dist/__tests__/funding-rates.test.d.ts +1 -0
  54. package/dist/__tests__/funding-rates.test.js +342 -0
  55. package/dist/__tests__/funding.test.d.ts +1 -0
  56. package/dist/__tests__/funding.test.js +173 -0
  57. package/dist/__tests__/gap-logic.test.d.ts +1 -0
  58. package/dist/__tests__/gap-logic.test.js +43 -0
  59. package/dist/__tests__/hip3-dex.test.d.ts +1 -0
  60. package/dist/__tests__/hip3-dex.test.js +234 -0
  61. package/dist/__tests__/integration/agent-features.integration.test.d.ts +1 -0
  62. package/dist/__tests__/integration/agent-features.integration.test.js +553 -0
  63. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +13 -0
  64. package/dist/__tests__/integration/atomic-commands.integration.test.js +246 -0
  65. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +1 -0
  66. package/dist/__tests__/integration/bridge-simulation.integration.test.js +453 -0
  67. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +1 -0
  68. package/dist/__tests__/integration/bridge-strict.integration.test.js +812 -0
  69. package/dist/__tests__/integration/bridge.integration.test.d.ts +1 -0
  70. package/dist/__tests__/integration/bridge.integration.test.js +309 -0
  71. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +1 -0
  72. package/dist/__tests__/integration/cli-e2e.integration.test.js +202 -0
  73. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +1 -0
  74. package/dist/__tests__/integration/dex-arb.integration.test.js +116 -0
  75. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +13 -0
  76. package/dist/__tests__/integration/envelope-consistency.integration.test.js +205 -0
  77. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +1 -0
  78. package/dist/__tests__/integration/hip3-dex.integration.test.js +147 -0
  79. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +1 -0
  80. package/dist/__tests__/integration/hyperliquid.integration.test.js +79 -0
  81. package/dist/__tests__/integration/lighter.integration.test.d.ts +1 -0
  82. package/dist/__tests__/integration/lighter.integration.test.js +53 -0
  83. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +9 -0
  84. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +236 -0
  85. package/dist/__tests__/integration/order-verification.integration.test.d.ts +1 -0
  86. package/dist/__tests__/integration/order-verification.integration.test.js +321 -0
  87. package/dist/__tests__/integration/pacifica.integration.test.d.ts +1 -0
  88. package/dist/__tests__/integration/pacifica.integration.test.js +75 -0
  89. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +1 -0
  90. package/dist/__tests__/integration/response-shapes.integration.test.js +278 -0
  91. package/dist/__tests__/liquidity.test.d.ts +1 -0
  92. package/dist/__tests__/liquidity.test.js +225 -0
  93. package/dist/__tests__/plan-executor.test.d.ts +1 -0
  94. package/dist/__tests__/plan-executor.test.js +314 -0
  95. package/dist/__tests__/position-history.test.d.ts +1 -0
  96. package/dist/__tests__/position-history.test.js +367 -0
  97. package/dist/__tests__/retry.test.d.ts +1 -0
  98. package/dist/__tests__/retry.test.js +310 -0
  99. package/dist/__tests__/risk-assessment.test.d.ts +1 -0
  100. package/dist/__tests__/risk-assessment.test.js +145 -0
  101. package/dist/__tests__/security-adversarial.test.d.ts +1 -0
  102. package/dist/__tests__/security-adversarial.test.js +574 -0
  103. package/dist/__tests__/strategies.test.d.ts +1 -0
  104. package/dist/__tests__/strategies.test.js +539 -0
  105. package/dist/__tests__/trade-execution.test.d.ts +1 -0
  106. package/dist/__tests__/trade-execution.test.js +129 -0
  107. package/dist/__tests__/trade-validator.test.d.ts +1 -0
  108. package/dist/__tests__/trade-validator.test.js +655 -0
  109. package/dist/__tests__/utils.test.d.ts +1 -0
  110. package/dist/__tests__/utils.test.js +76 -0
  111. package/dist/api/public/hyperliquid.d.ts +18 -0
  112. package/dist/api/public/hyperliquid.js +82 -0
  113. package/dist/api/public/index.d.ts +8 -0
  114. package/dist/api/public/index.js +8 -0
  115. package/dist/api/public/lighter.d.ts +24 -0
  116. package/dist/api/public/lighter.js +100 -0
  117. package/dist/api/public/pacifica.d.ts +17 -0
  118. package/dist/api/public/pacifica.js +54 -0
  119. package/dist/api/public/urls.d.ts +12 -0
  120. package/dist/api/public/urls.js +33 -0
  121. package/dist/arb/history-stats.d.ts +44 -0
  122. package/dist/arb/history-stats.js +135 -0
  123. package/dist/arb/index.d.ts +4 -0
  124. package/dist/arb/index.js +4 -0
  125. package/dist/arb/sizing.d.ts +23 -0
  126. package/dist/arb/sizing.js +96 -0
  127. package/dist/arb/state.d.ts +51 -0
  128. package/dist/arb/state.js +112 -0
  129. package/dist/arb/utils.d.ts +81 -0
  130. package/dist/arb/utils.js +267 -0
  131. package/dist/arb-history-stats.d.ts +5 -0
  132. package/dist/arb-history-stats.js +5 -0
  133. package/dist/arb-sizing.d.ts +5 -0
  134. package/dist/arb-sizing.js +5 -0
  135. package/dist/arb-state.d.ts +5 -0
  136. package/dist/arb-state.js +5 -0
  137. package/dist/arb-utils.d.ts +5 -0
  138. package/dist/arb-utils.js +5 -0
  139. package/dist/bot/conditions.d.ts +32 -0
  140. package/dist/bot/conditions.js +141 -0
  141. package/dist/bot/config.d.ts +76 -0
  142. package/dist/bot/config.js +160 -0
  143. package/dist/bot/engine.d.ts +8 -0
  144. package/dist/bot/engine.js +519 -0
  145. package/dist/bot/presets.d.ts +11 -0
  146. package/dist/bot/presets.js +296 -0
  147. package/dist/bridge-engine.d.ts +133 -0
  148. package/dist/bridge-engine.js +1487 -0
  149. package/dist/cache.d.ts +25 -0
  150. package/dist/cache.js +99 -0
  151. package/dist/cli-spec.d.ts +50 -0
  152. package/dist/cli-spec.js +75 -0
  153. package/dist/client-id-tracker.d.ts +25 -0
  154. package/dist/client-id-tracker.js +76 -0
  155. package/dist/commands/account.d.ts +3 -0
  156. package/dist/commands/account.js +425 -0
  157. package/dist/commands/agent.d.ts +3 -0
  158. package/dist/commands/agent.js +386 -0
  159. package/dist/commands/alert.d.ts +2 -0
  160. package/dist/commands/alert.js +421 -0
  161. package/dist/commands/analytics.d.ts +3 -0
  162. package/dist/commands/analytics.js +311 -0
  163. package/dist/commands/arb/index.d.ts +3 -0
  164. package/dist/commands/arb/index.js +921 -0
  165. package/dist/commands/arb-auto.d.ts +54 -0
  166. package/dist/commands/arb-auto.js +1328 -0
  167. package/dist/commands/arb-manage.d.ts +5 -0
  168. package/dist/commands/arb-manage.js +5 -0
  169. package/dist/commands/arb.d.ts +2 -0
  170. package/dist/commands/arb.js +347 -0
  171. package/dist/commands/backtest.d.ts +2 -0
  172. package/dist/commands/backtest.js +327 -0
  173. package/dist/commands/bot.d.ts +3 -0
  174. package/dist/commands/bot.js +412 -0
  175. package/dist/commands/bridge.d.ts +2 -0
  176. package/dist/commands/bridge.js +396 -0
  177. package/dist/commands/dashboard.d.ts +3 -0
  178. package/dist/commands/dashboard.js +176 -0
  179. package/dist/commands/deposit.d.ts +4 -0
  180. package/dist/commands/deposit.js +573 -0
  181. package/dist/commands/dex.d.ts +3 -0
  182. package/dist/commands/dex.js +114 -0
  183. package/dist/commands/env.d.ts +2 -0
  184. package/dist/commands/env.js +136 -0
  185. package/dist/commands/funding.d.ts +2 -0
  186. package/dist/commands/funding.js +347 -0
  187. package/dist/commands/gap.d.ts +2 -0
  188. package/dist/commands/gap.js +305 -0
  189. package/dist/commands/health.d.ts +2 -0
  190. package/dist/commands/health.js +67 -0
  191. package/dist/commands/history.d.ts +2 -0
  192. package/dist/commands/history.js +235 -0
  193. package/dist/commands/init.d.ts +15 -0
  194. package/dist/commands/init.js +266 -0
  195. package/dist/commands/jobs.d.ts +2 -0
  196. package/dist/commands/jobs.js +133 -0
  197. package/dist/commands/manage.d.ts +4 -0
  198. package/dist/commands/manage.js +309 -0
  199. package/dist/commands/market.d.ts +3 -0
  200. package/dist/commands/market.js +225 -0
  201. package/dist/commands/plan.d.ts +3 -0
  202. package/dist/commands/plan.js +95 -0
  203. package/dist/commands/portfolio.d.ts +3 -0
  204. package/dist/commands/portfolio.js +169 -0
  205. package/dist/commands/rebalance.d.ts +3 -0
  206. package/dist/commands/rebalance.js +293 -0
  207. package/dist/commands/risk.d.ts +3 -0
  208. package/dist/commands/risk.js +169 -0
  209. package/dist/commands/run.d.ts +3 -0
  210. package/dist/commands/run.js +202 -0
  211. package/dist/commands/settings.d.ts +2 -0
  212. package/dist/commands/settings.js +102 -0
  213. package/dist/commands/stream.d.ts +5 -0
  214. package/dist/commands/stream.js +123 -0
  215. package/dist/commands/trade.d.ts +3 -0
  216. package/dist/commands/trade.js +1273 -0
  217. package/dist/commands/wallet.d.ts +14 -0
  218. package/dist/commands/wallet.js +602 -0
  219. package/dist/commands/withdraw.d.ts +3 -0
  220. package/dist/commands/withdraw.js +187 -0
  221. package/dist/config.d.ts +5 -0
  222. package/dist/config.js +68 -0
  223. package/dist/cross-chain-margin.d.ts +46 -0
  224. package/dist/cross-chain-margin.js +107 -0
  225. package/dist/dashboard/server.d.ts +80 -0
  226. package/dist/dashboard/server.js +340 -0
  227. package/dist/dashboard/ui.d.ts +4 -0
  228. package/dist/dashboard/ui.js +538 -0
  229. package/dist/dashboard/ws-feeds.d.ts +29 -0
  230. package/dist/dashboard/ws-feeds.js +660 -0
  231. package/dist/dex-asset-map.d.ts +80 -0
  232. package/dist/dex-asset-map.js +201 -0
  233. package/dist/errors.d.ts +109 -0
  234. package/dist/errors.js +84 -0
  235. package/dist/event-stream.d.ts +25 -0
  236. package/dist/event-stream.js +168 -0
  237. package/dist/exchanges/hyperliquid.d.ts +212 -0
  238. package/dist/exchanges/hyperliquid.js +931 -0
  239. package/dist/exchanges/interface.d.ts +95 -0
  240. package/dist/exchanges/interface.js +5 -0
  241. package/dist/exchanges/lighter.d.ts +159 -0
  242. package/dist/exchanges/lighter.js +793 -0
  243. package/dist/exchanges/pacifica.d.ts +51 -0
  244. package/dist/exchanges/pacifica.js +248 -0
  245. package/dist/execution-log.d.ts +36 -0
  246. package/dist/execution-log.js +102 -0
  247. package/dist/funding/history.d.ts +63 -0
  248. package/dist/funding/history.js +266 -0
  249. package/dist/funding/index.d.ts +3 -0
  250. package/dist/funding/index.js +3 -0
  251. package/dist/funding/normalize.d.ts +39 -0
  252. package/dist/funding/normalize.js +66 -0
  253. package/dist/funding/rates.d.ts +45 -0
  254. package/dist/funding/rates.js +172 -0
  255. package/dist/funding-history.d.ts +5 -0
  256. package/dist/funding-history.js +5 -0
  257. package/dist/funding-rates.d.ts +5 -0
  258. package/dist/funding-rates.js +5 -0
  259. package/dist/funding.d.ts +5 -0
  260. package/dist/funding.js +5 -0
  261. package/dist/index.d.ts +2 -0
  262. package/dist/index.js +458 -0
  263. package/dist/jobs.d.ts +37 -0
  264. package/dist/jobs.js +152 -0
  265. package/dist/liquidity.d.ts +34 -0
  266. package/dist/liquidity.js +100 -0
  267. package/dist/mcp-server.d.ts +9 -0
  268. package/dist/mcp-server.js +1206 -0
  269. package/dist/pacifica/client.d.ts +111 -0
  270. package/dist/pacifica/client.js +310 -0
  271. package/dist/pacifica/constants.d.ts +27 -0
  272. package/dist/pacifica/constants.js +47 -0
  273. package/dist/pacifica/deposit.d.ts +14 -0
  274. package/dist/pacifica/deposit.js +78 -0
  275. package/dist/pacifica/index.d.ts +6 -0
  276. package/dist/pacifica/index.js +11 -0
  277. package/dist/pacifica/signing.d.ts +49 -0
  278. package/dist/pacifica/signing.js +97 -0
  279. package/dist/pacifica/types/account.d.ts +42 -0
  280. package/dist/pacifica/types/account.js +1 -0
  281. package/dist/pacifica/types/index.d.ts +6 -0
  282. package/dist/pacifica/types/index.js +6 -0
  283. package/dist/pacifica/types/lake.d.ts +18 -0
  284. package/dist/pacifica/types/lake.js +1 -0
  285. package/dist/pacifica/types/market.d.ts +64 -0
  286. package/dist/pacifica/types/market.js +1 -0
  287. package/dist/pacifica/types/order.d.ts +92 -0
  288. package/dist/pacifica/types/order.js +1 -0
  289. package/dist/pacifica/types/position.d.ts +25 -0
  290. package/dist/pacifica/types/position.js +1 -0
  291. package/dist/pacifica/types/ws.d.ts +34 -0
  292. package/dist/pacifica/types/ws.js +41 -0
  293. package/dist/pacifica/ws-client.d.ts +42 -0
  294. package/dist/pacifica/ws-client.js +180 -0
  295. package/dist/plan-executor.d.ts +48 -0
  296. package/dist/plan-executor.js +280 -0
  297. package/dist/position-history.d.ts +68 -0
  298. package/dist/position-history.js +222 -0
  299. package/dist/rebalance.d.ts +64 -0
  300. package/dist/rebalance.js +142 -0
  301. package/dist/retry.d.ts +74 -0
  302. package/dist/retry.js +129 -0
  303. package/dist/risk.d.ts +48 -0
  304. package/dist/risk.js +156 -0
  305. package/dist/settings.d.ts +19 -0
  306. package/dist/settings.js +45 -0
  307. package/dist/shared-api.d.ts +5 -0
  308. package/dist/shared-api.js +5 -0
  309. package/dist/strategies/dca.d.ts +25 -0
  310. package/dist/strategies/dca.js +114 -0
  311. package/dist/strategies/funding-arb.d.ts +15 -0
  312. package/dist/strategies/funding-arb.js +281 -0
  313. package/dist/strategies/grid.d.ts +34 -0
  314. package/dist/strategies/grid.js +185 -0
  315. package/dist/strategies/trailing-stop.d.ts +17 -0
  316. package/dist/strategies/trailing-stop.js +121 -0
  317. package/dist/strategies/twap.d.ts +20 -0
  318. package/dist/strategies/twap.js +78 -0
  319. package/dist/trade-validator.d.ts +39 -0
  320. package/dist/trade-validator.js +154 -0
  321. package/dist/utils.d.ts +38 -0
  322. package/dist/utils.js +110 -0
  323. package/package.json +63 -0
  324. package/skills/perp-cli/SKILL.md +149 -0
  325. package/skills/perp-cli/references/commands.md +143 -0
@@ -0,0 +1,821 @@
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
+ });