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,600 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { executePlan } from "../../plan-executor.js";
3
+ vi.mock("../../execution-log.js", () => ({ logExecution: vi.fn() }));
4
+ // ---------------------------------------------------------------------------
5
+ // Mock adapter factory
6
+ // ---------------------------------------------------------------------------
7
+ function createMockAdapter(overrides) {
8
+ const positions = overrides?.positions ?? [];
9
+ const balance = {
10
+ equity: "10000",
11
+ available: "8000",
12
+ marginUsed: "2000",
13
+ unrealizedPnl: "0",
14
+ ...overrides?.balance,
15
+ };
16
+ return {
17
+ name: "mock",
18
+ getMarkets: vi.fn().mockResolvedValue([]),
19
+ getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
20
+ getRecentTrades: vi.fn().mockResolvedValue([]),
21
+ getFundingHistory: vi.fn().mockResolvedValue([]),
22
+ getKlines: vi.fn().mockResolvedValue([]),
23
+ getBalance: vi.fn().mockResolvedValue(balance),
24
+ getPositions: vi.fn().mockResolvedValue(positions),
25
+ getOpenOrders: vi.fn().mockResolvedValue([]),
26
+ getOrderHistory: vi.fn().mockResolvedValue([]),
27
+ getTradeHistory: vi.fn().mockResolvedValue([]),
28
+ getFundingPayments: vi.fn().mockResolvedValue([]),
29
+ marketOrder: vi.fn().mockResolvedValue({ orderId: "m-1" }),
30
+ limitOrder: vi.fn().mockResolvedValue({ orderId: "l-1" }),
31
+ editOrder: vi.fn().mockResolvedValue({ orderId: "e-1" }),
32
+ cancelOrder: vi.fn().mockResolvedValue({ cancelled: true }),
33
+ cancelAllOrders: vi.fn().mockResolvedValue({ cancelled: true }),
34
+ setLeverage: vi.fn().mockResolvedValue({ ok: true }),
35
+ stopOrder: vi.fn().mockResolvedValue({ orderId: "s-1" }),
36
+ };
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // 1. Open long BTC with stop loss
40
+ // ---------------------------------------------------------------------------
41
+ describe("Scenario 1: Open long BTC with stop loss", () => {
42
+ it("executes setLeverage -> marketOrder (buy) -> stopOrder (sell) in dependency order", async () => {
43
+ const adapter = createMockAdapter();
44
+ const plan = {
45
+ version: "1.0",
46
+ steps: [
47
+ { id: "lev", action: "set_leverage", params: { symbol: "BTC", leverage: 10 } },
48
+ { id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, dependsOn: "lev" },
49
+ { id: "sl", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "60000" }, dependsOn: "buy" },
50
+ ],
51
+ };
52
+ const result = await executePlan(adapter, plan);
53
+ expect(result.status).toBe("completed");
54
+ expect(result.steps).toHaveLength(3);
55
+ expect(result.steps.every(s => s.status === "success")).toBe(true);
56
+ // setLeverage: symbol, leverage, marginMode (default "cross")
57
+ expect(adapter.setLeverage).toHaveBeenCalledTimes(1);
58
+ expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 10, "cross");
59
+ // marketOrder: symbol, side, size
60
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
61
+ expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "buy", "0.01");
62
+ // stopOrder: symbol, side, size, triggerPrice, opts
63
+ // CRITICAL: stop loss side is SELL (opposite of long)
64
+ expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
65
+ expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.01", "60000", { limitPrice: undefined, reduceOnly: false });
66
+ // Verify call order
67
+ const setLevOrder = adapter.setLeverage.mock.invocationCallOrder[0];
68
+ const buyOrder = adapter.marketOrder.mock.invocationCallOrder[0];
69
+ const slOrder = adapter.stopOrder.mock.invocationCallOrder[0];
70
+ expect(setLevOrder).toBeLessThan(buyOrder);
71
+ expect(buyOrder).toBeLessThan(slOrder);
72
+ });
73
+ });
74
+ // ---------------------------------------------------------------------------
75
+ // 2. Delta-neutral hedge
76
+ // ---------------------------------------------------------------------------
77
+ describe("Scenario 2: Delta-neutral hedge", () => {
78
+ it("opens long BTC and short ETH with correct sides", async () => {
79
+ const adapter = createMockAdapter();
80
+ const plan = {
81
+ version: "1.0",
82
+ steps: [
83
+ { id: "long", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
84
+ { id: "short", action: "market_order", params: { symbol: "ETH", side: "sell", size: "0.1" } },
85
+ ],
86
+ };
87
+ const result = await executePlan(adapter, plan);
88
+ expect(result.status).toBe("completed");
89
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
90
+ // First call: long BTC
91
+ expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "buy", "0.01");
92
+ // Second call: short ETH
93
+ expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "sell", "0.1");
94
+ });
95
+ });
96
+ // ---------------------------------------------------------------------------
97
+ // 3. Emergency close all
98
+ // ---------------------------------------------------------------------------
99
+ describe("Scenario 3: Emergency close all", () => {
100
+ it("cancels all orders then closes BTC long by selling", async () => {
101
+ const adapter = createMockAdapter({
102
+ positions: [
103
+ {
104
+ symbol: "BTC",
105
+ side: "long",
106
+ size: "0.5",
107
+ entryPrice: "65000",
108
+ markPrice: "64000",
109
+ liquidationPrice: "55000",
110
+ unrealizedPnl: "-500",
111
+ leverage: 10,
112
+ },
113
+ ],
114
+ });
115
+ const plan = {
116
+ version: "1.0",
117
+ steps: [
118
+ { id: "cancel", action: "cancel_all", params: {} },
119
+ { id: "close", action: "close_position", params: { symbol: "BTC" }, dependsOn: "cancel" },
120
+ ],
121
+ };
122
+ const result = await executePlan(adapter, plan);
123
+ expect(result.status).toBe("completed");
124
+ expect(result.steps).toHaveLength(2);
125
+ // cancelAllOrders called first
126
+ expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(1);
127
+ // close_position fetches positions, then sells the long
128
+ expect(adapter.getPositions).toHaveBeenCalledTimes(1);
129
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
130
+ // CRITICAL: closing a LONG = must SELL, not buy
131
+ expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
132
+ // Verify order: cancel before market order
133
+ const cancelCallOrder = adapter.cancelAllOrders.mock.invocationCallOrder[0];
134
+ const marketCallOrder = adapter.marketOrder.mock.invocationCallOrder[0];
135
+ expect(cancelCallOrder).toBeLessThan(marketCallOrder);
136
+ });
137
+ });
138
+ // ---------------------------------------------------------------------------
139
+ // 4. Scale in: multiple buys with wait
140
+ // ---------------------------------------------------------------------------
141
+ describe("Scenario 4: Scale in with wait", () => {
142
+ it("buys ETH twice with a wait pause in between, then checks position", async () => {
143
+ const adapter = createMockAdapter({
144
+ positions: [
145
+ {
146
+ symbol: "ETH",
147
+ side: "long",
148
+ size: "2",
149
+ entryPrice: "3000",
150
+ markPrice: "3100",
151
+ liquidationPrice: "2500",
152
+ unrealizedPnl: "200",
153
+ leverage: 5,
154
+ },
155
+ ],
156
+ });
157
+ const plan = {
158
+ version: "1.0",
159
+ steps: [
160
+ { id: "buy1", action: "market_order", params: { symbol: "ETH", side: "buy", size: "1" } },
161
+ { id: "wait", action: "wait", params: { ms: 100 }, dependsOn: "buy1" },
162
+ { id: "buy2", action: "market_order", params: { symbol: "ETH", side: "buy", size: "1" }, dependsOn: "wait" },
163
+ { id: "check", action: "check_position", params: { symbol: "ETH", mustExist: true }, dependsOn: "buy2" },
164
+ ],
165
+ };
166
+ const before = Date.now();
167
+ const result = await executePlan(adapter, plan);
168
+ const elapsed = Date.now() - before;
169
+ expect(result.status).toBe("completed");
170
+ expect(result.steps).toHaveLength(4);
171
+ expect(result.steps.every(s => s.status === "success")).toBe(true);
172
+ // marketOrder called exactly twice, both buy ETH 1
173
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
174
+ expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "ETH", "buy", "1");
175
+ expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "ETH", "buy", "1");
176
+ // Wait actually paused at least ~100ms
177
+ expect(elapsed).toBeGreaterThanOrEqual(90); // allow small timing margin
178
+ // check_position succeeded (position exists)
179
+ expect(adapter.getPositions).toHaveBeenCalled();
180
+ const checkStep = result.steps.find(s => s.stepId === "check");
181
+ expect(checkStep?.status).toBe("success");
182
+ });
183
+ });
184
+ // ---------------------------------------------------------------------------
185
+ // 5. Rollback on failure
186
+ // ---------------------------------------------------------------------------
187
+ describe("Scenario 5: Rollback on failure", () => {
188
+ it("rolls back (cancelAllOrders) when a step with onFailure=rollback throws", async () => {
189
+ const adapter = createMockAdapter();
190
+ // limitOrder will fail
191
+ adapter.limitOrder.mockRejectedValueOnce(new Error("Insufficient margin"));
192
+ const plan = {
193
+ version: "1.0",
194
+ steps: [
195
+ { id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
196
+ { id: "fail", action: "limit_order", params: { symbol: "ETH", side: "sell", size: "1", price: "2000" }, dependsOn: "buy", onFailure: "rollback" },
197
+ ],
198
+ };
199
+ const result = await executePlan(adapter, plan);
200
+ // Overall status is failed
201
+ expect(result.status).toBe("failed");
202
+ // Step 1 succeeded
203
+ const buyStep = result.steps.find(s => s.stepId === "buy");
204
+ expect(buyStep?.status).toBe("success");
205
+ // Step 2 rolled back
206
+ const failStep = result.steps.find(s => s.stepId === "fail");
207
+ expect(failStep?.status).toBe("rolled_back");
208
+ expect(failStep?.error?.message).toContain("Insufficient margin");
209
+ // Rollback called cancelAllOrders (the rollback function cancels all for market_order steps)
210
+ expect(adapter.cancelAllOrders).toHaveBeenCalled();
211
+ // marketOrder was called once (the buy), limitOrder was called once (the failing sell)
212
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
213
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
214
+ });
215
+ });
216
+ // ---------------------------------------------------------------------------
217
+ // 6. Skip non-critical step
218
+ // ---------------------------------------------------------------------------
219
+ describe("Scenario 6: Skip non-critical step", () => {
220
+ it("continues execution when a step with onFailure=skip fails", async () => {
221
+ const adapter = createMockAdapter();
222
+ // stopOrder will fail
223
+ adapter.stopOrder.mockRejectedValueOnce(new Error("Rate limit exceeded"));
224
+ const plan = {
225
+ version: "1.0",
226
+ steps: [
227
+ { id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
228
+ { id: "tp", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "100000" }, onFailure: "skip" },
229
+ { id: "check", action: "check_balance", params: {} },
230
+ ],
231
+ };
232
+ const result = await executePlan(adapter, plan);
233
+ // Overall: completed (skip doesn't cause failure)
234
+ expect(result.status).toBe("completed");
235
+ expect(result.steps).toHaveLength(3);
236
+ // Step 1 succeeded
237
+ expect(result.steps[0].status).toBe("success");
238
+ // Step 2 was skipped
239
+ expect(result.steps[1].status).toBe("skipped");
240
+ expect(result.steps[1].error?.message).toContain("Rate limit exceeded");
241
+ // Step 3 still executed successfully
242
+ expect(result.steps[2].status).toBe("success");
243
+ expect(adapter.getBalance).toHaveBeenCalledTimes(1);
244
+ });
245
+ });
246
+ // ---------------------------------------------------------------------------
247
+ // 7. Balance gate
248
+ // ---------------------------------------------------------------------------
249
+ describe("Scenario 7: Balance gate - don't trade if balance too low", () => {
250
+ it("aborts the plan when check_balance fails with onFailure=abort", async () => {
251
+ const adapter = createMockAdapter({
252
+ balance: { available: "800", equity: "800", marginUsed: "0", unrealizedPnl: "0" },
253
+ });
254
+ const plan = {
255
+ version: "1.0",
256
+ steps: [
257
+ { id: "gate", action: "check_balance", params: { minAvailable: 10000 }, onFailure: "abort" },
258
+ { id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "1" }, dependsOn: "gate" },
259
+ ],
260
+ };
261
+ const result = await executePlan(adapter, plan);
262
+ // Plan failed/aborted at the gate
263
+ expect(result.status).toBe("failed");
264
+ // Gate step failed
265
+ const gateStep = result.steps.find(s => s.stepId === "gate");
266
+ expect(gateStep?.status).toBe("failed");
267
+ expect(gateStep?.error?.message).toContain("$800");
268
+ expect(gateStep?.error?.message).toContain("$10000");
269
+ // CRITICAL: marketOrder was NEVER called — the safety gate worked
270
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(0);
271
+ });
272
+ });
273
+ // ---------------------------------------------------------------------------
274
+ // 8. Close position side correctness (CRITICAL)
275
+ // ---------------------------------------------------------------------------
276
+ describe("Scenario 8: Close position side correctness", () => {
277
+ it("closes a LONG position by SELLING (not buying)", async () => {
278
+ const adapter = createMockAdapter({
279
+ positions: [
280
+ {
281
+ symbol: "BTC",
282
+ side: "long",
283
+ size: "0.5",
284
+ entryPrice: "65000",
285
+ markPrice: "66000",
286
+ liquidationPrice: "55000",
287
+ unrealizedPnl: "500",
288
+ leverage: 10,
289
+ },
290
+ ],
291
+ });
292
+ const plan = {
293
+ version: "1.0",
294
+ steps: [
295
+ { id: "close", action: "close_position", params: { symbol: "BTC" } },
296
+ ],
297
+ };
298
+ const result = await executePlan(adapter, plan);
299
+ expect(result.status).toBe("completed");
300
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
301
+ // MUST be sell to close a long — if this were "buy", user doubles their position!
302
+ expect(adapter.marketOrder).toHaveBeenCalledWith("BTC", "sell", "0.5");
303
+ });
304
+ it("closes a SHORT position by BUYING (not selling)", async () => {
305
+ const adapter = createMockAdapter({
306
+ positions: [
307
+ {
308
+ symbol: "ETH",
309
+ side: "short",
310
+ size: "2.0",
311
+ entryPrice: "3500",
312
+ markPrice: "3400",
313
+ liquidationPrice: "4000",
314
+ unrealizedPnl: "200",
315
+ leverage: 5,
316
+ },
317
+ ],
318
+ });
319
+ const plan = {
320
+ version: "1.0",
321
+ steps: [
322
+ { id: "close", action: "close_position", params: { symbol: "ETH" } },
323
+ ],
324
+ };
325
+ const result = await executePlan(adapter, plan);
326
+ expect(result.status).toBe("completed");
327
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
328
+ // MUST be buy to close a short — if this were "sell", user doubles their position!
329
+ expect(adapter.marketOrder).toHaveBeenCalledWith("ETH", "buy", "2.0");
330
+ });
331
+ it("fails gracefully when closing a non-existent position", async () => {
332
+ const adapter = createMockAdapter({ positions: [] });
333
+ const plan = {
334
+ version: "1.0",
335
+ steps: [
336
+ { id: "close", action: "close_position", params: { symbol: "SOL" } },
337
+ ],
338
+ };
339
+ const result = await executePlan(adapter, plan);
340
+ // Default onFailure is "abort"
341
+ expect(result.status).toBe("failed");
342
+ const closeStep = result.steps.find(s => s.stepId === "close");
343
+ expect(closeStep?.status).toBe("failed");
344
+ expect(closeStep?.error?.message).toContain("No position found for SOL");
345
+ // No market order placed
346
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(0);
347
+ });
348
+ });
349
+ // ---------------------------------------------------------------------------
350
+ // 9. Dry run doesn't execute
351
+ // ---------------------------------------------------------------------------
352
+ describe("Scenario 9: Dry run doesn't execute", () => {
353
+ it("returns dry_run status without calling any adapter trading methods", async () => {
354
+ const adapter = createMockAdapter();
355
+ const plan = {
356
+ version: "1.0",
357
+ steps: [
358
+ { id: "lev", action: "set_leverage", params: { symbol: "BTC", leverage: 10 } },
359
+ { id: "buy", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, dependsOn: "lev" },
360
+ { id: "sl", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "60000" }, dependsOn: "buy" },
361
+ ],
362
+ };
363
+ const result = await executePlan(adapter, plan, { dryRun: true });
364
+ // Overall status is dry_run
365
+ expect(result.status).toBe("dry_run");
366
+ expect(result.steps).toHaveLength(3);
367
+ expect(result.steps.every(s => s.status === "dry_run")).toBe(true);
368
+ // ALL adapter methods have 0 calls
369
+ expect(adapter.setLeverage).toHaveBeenCalledTimes(0);
370
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(0);
371
+ expect(adapter.stopOrder).toHaveBeenCalledTimes(0);
372
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(0);
373
+ expect(adapter.cancelOrder).toHaveBeenCalledTimes(0);
374
+ expect(adapter.cancelAllOrders).toHaveBeenCalledTimes(0);
375
+ expect(adapter.getPositions).toHaveBeenCalledTimes(0);
376
+ expect(adapter.getBalance).toHaveBeenCalledTimes(0);
377
+ // Each step records what it would have done
378
+ for (const step of result.steps) {
379
+ expect(step.result.wouldExecute).toBe(true);
380
+ }
381
+ });
382
+ });
383
+ // ---------------------------------------------------------------------------
384
+ // 10. Dependency chain failure propagation
385
+ // ---------------------------------------------------------------------------
386
+ describe("Scenario 10: Dependency chain failure propagation", () => {
387
+ it("skips dependent steps when a step fails with default abort", async () => {
388
+ const adapter = createMockAdapter();
389
+ // Step A (market_order) will fail
390
+ adapter.marketOrder.mockRejectedValueOnce(new Error("Exchange down"));
391
+ const plan = {
392
+ version: "1.0",
393
+ steps: [
394
+ { id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
395
+ { id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
396
+ { id: "C", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "55000" }, dependsOn: "B" },
397
+ ],
398
+ };
399
+ const result = await executePlan(adapter, plan);
400
+ // A fails with default abort -> plan stops immediately, B and C never run
401
+ expect(result.status).toBe("failed");
402
+ // Only A is in the results (abort exits immediately)
403
+ expect(result.steps).toHaveLength(1);
404
+ expect(result.steps[0].stepId).toBe("A");
405
+ expect(result.steps[0].status).toBe("failed");
406
+ // B and C never executed
407
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(0);
408
+ expect(adapter.stopOrder).toHaveBeenCalledTimes(0);
409
+ });
410
+ it("skips A but still runs B (dependsOn skipped step is not treated as failed) and C", async () => {
411
+ const adapter = createMockAdapter();
412
+ // Step A will fail but is marked skip
413
+ adapter.marketOrder.mockRejectedValueOnce(new Error("Exchange down"));
414
+ const plan = {
415
+ version: "1.0",
416
+ steps: [
417
+ { id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, onFailure: "skip" },
418
+ { id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
419
+ { id: "C", action: "check_balance", params: {} },
420
+ ],
421
+ };
422
+ const result = await executePlan(adapter, plan);
423
+ // The executor dependency check only blocks on dep.status === "failed", NOT "skipped".
424
+ // So A is skipped (not "failed"), B's dependency sees a non-failed dep and proceeds, C runs too.
425
+ expect(result.steps).toHaveLength(3);
426
+ expect(result.steps[0].stepId).toBe("A");
427
+ expect(result.steps[0].status).toBe("skipped");
428
+ // B runs because "skipped" !== "failed" in the dependency check
429
+ expect(result.steps[1].stepId).toBe("B");
430
+ expect(result.steps[1].status).toBe("success");
431
+ expect(result.steps[2].stepId).toBe("C");
432
+ expect(result.steps[2].status).toBe("success");
433
+ // marketOrder called once (A, which failed), limitOrder called once (B succeeded)
434
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
435
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
436
+ expect(adapter.limitOrder).toHaveBeenCalledWith("BTC", "buy", "60000", "0.02");
437
+ expect(adapter.getBalance).toHaveBeenCalledTimes(1);
438
+ });
439
+ it("blocks dependsOn when a step actually fails (status=failed), not skipped", async () => {
440
+ const adapter = createMockAdapter();
441
+ // Step A will fail with default onFailure=abort... but we need "failed" status recorded.
442
+ // With abort, the executor returns immediately so B never enters.
443
+ // To get a "failed" status in completedSteps while continuing, we need a special setup:
444
+ // A fails+abort -> only A in results, plan stops. B never runs.
445
+ adapter.marketOrder.mockRejectedValueOnce(new Error("Exchange down"));
446
+ const plan = {
447
+ version: "1.0",
448
+ steps: [
449
+ { id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" } },
450
+ { id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
451
+ ],
452
+ };
453
+ const result = await executePlan(adapter, plan);
454
+ // Default onFailure=abort causes immediate return
455
+ expect(result.status).toBe("failed");
456
+ expect(result.steps).toHaveLength(1);
457
+ expect(result.steps[0].status).toBe("failed");
458
+ // B never ran
459
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(0);
460
+ });
461
+ it("skipped status does NOT propagate through dependency chain: A(skip) -> B(dep A) -> C(dep B) all run", async () => {
462
+ const adapter = createMockAdapter();
463
+ adapter.marketOrder.mockRejectedValueOnce(new Error("fail"));
464
+ const plan = {
465
+ version: "1.0",
466
+ steps: [
467
+ { id: "A", action: "market_order", params: { symbol: "BTC", side: "buy", size: "0.01" }, onFailure: "skip" },
468
+ { id: "B", action: "limit_order", params: { symbol: "BTC", side: "buy", size: "0.02", price: "60000" }, dependsOn: "A" },
469
+ { id: "C", action: "stop_order", params: { symbol: "BTC", side: "sell", size: "0.01", triggerPrice: "55000" }, dependsOn: "B" },
470
+ ],
471
+ };
472
+ const result = await executePlan(adapter, plan);
473
+ // The executor only blocks on dep.status === "failed". "skipped" is NOT "failed",
474
+ // so B proceeds (dep A exists and is not failed), and C proceeds (dep B exists and is success).
475
+ expect(result.status).toBe("completed");
476
+ expect(result.steps).toHaveLength(3);
477
+ expect(result.steps[0].status).toBe("skipped"); // A failed but skipped
478
+ expect(result.steps[1].status).toBe("success"); // B runs (A's "skipped" != "failed")
479
+ expect(result.steps[2].status).toBe("success"); // C runs (B succeeded)
480
+ // A's market order attempted (failed), B's limit order ran, C's stop order ran
481
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(1);
482
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(1);
483
+ expect(adapter.stopOrder).toHaveBeenCalledTimes(1);
484
+ });
485
+ });
486
+ // ---------------------------------------------------------------------------
487
+ // Additional edge cases
488
+ // ---------------------------------------------------------------------------
489
+ describe("Edge cases", () => {
490
+ it("cancel_all with empty symbol passes undefined", async () => {
491
+ const adapter = createMockAdapter();
492
+ const plan = {
493
+ version: "1.0",
494
+ steps: [
495
+ { id: "c", action: "cancel_all", params: {} },
496
+ ],
497
+ };
498
+ await executePlan(adapter, plan);
499
+ // Empty string from params -> cancelAllOrders(undefined)
500
+ expect(adapter.cancelAllOrders).toHaveBeenCalledWith(undefined);
501
+ });
502
+ it("set_leverage uses isolated margin mode when specified", async () => {
503
+ const adapter = createMockAdapter();
504
+ const plan = {
505
+ version: "1.0",
506
+ steps: [
507
+ { id: "lev", action: "set_leverage", params: { symbol: "ETH", leverage: 20, marginMode: "isolated" } },
508
+ ],
509
+ };
510
+ await executePlan(adapter, plan);
511
+ expect(adapter.setLeverage).toHaveBeenCalledWith("ETH", 20, "isolated");
512
+ });
513
+ it("check_position with mustExist=true throws when position absent", async () => {
514
+ const adapter = createMockAdapter({ positions: [] });
515
+ const plan = {
516
+ version: "1.0",
517
+ steps: [
518
+ { id: "chk", action: "check_position", params: { symbol: "SOL", mustExist: true } },
519
+ ],
520
+ };
521
+ const result = await executePlan(adapter, plan);
522
+ expect(result.status).toBe("failed");
523
+ const step = result.steps[0];
524
+ expect(step.status).toBe("failed");
525
+ expect(step.error?.message).toContain("SOL");
526
+ expect(step.error?.message).toContain("not found");
527
+ });
528
+ it("limit_order passes all arguments correctly", async () => {
529
+ const adapter = createMockAdapter();
530
+ const plan = {
531
+ version: "1.0",
532
+ steps: [
533
+ { id: "lim", action: "limit_order", params: { symbol: "ETH", side: "buy", price: "3000", size: "2.5" } },
534
+ ],
535
+ };
536
+ await executePlan(adapter, plan);
537
+ expect(adapter.limitOrder).toHaveBeenCalledWith("ETH", "buy", "3000", "2.5");
538
+ });
539
+ it("stop_order passes limitPrice and reduceOnly options", async () => {
540
+ const adapter = createMockAdapter();
541
+ const plan = {
542
+ version: "1.0",
543
+ steps: [
544
+ {
545
+ id: "stop",
546
+ action: "stop_order",
547
+ params: {
548
+ symbol: "BTC",
549
+ side: "sell",
550
+ size: "0.1",
551
+ triggerPrice: "58000",
552
+ limitPrice: "57500",
553
+ reduceOnly: true,
554
+ },
555
+ },
556
+ ],
557
+ };
558
+ await executePlan(adapter, plan);
559
+ expect(adapter.stopOrder).toHaveBeenCalledWith("BTC", "sell", "0.1", "58000", { limitPrice: "57500", reduceOnly: true });
560
+ });
561
+ it("cancel_order passes symbol and orderId", async () => {
562
+ const adapter = createMockAdapter();
563
+ const plan = {
564
+ version: "1.0",
565
+ steps: [
566
+ { id: "cx", action: "cancel_order", params: { symbol: "BTC", orderId: "order-abc-123" } },
567
+ ],
568
+ };
569
+ await executePlan(adapter, plan);
570
+ expect(adapter.cancelOrder).toHaveBeenCalledWith("BTC", "order-abc-123");
571
+ });
572
+ it("symbols are uppercased consistently", async () => {
573
+ const adapter = createMockAdapter({
574
+ positions: [
575
+ {
576
+ symbol: "BTC",
577
+ side: "long",
578
+ size: "1",
579
+ entryPrice: "65000",
580
+ markPrice: "65000",
581
+ liquidationPrice: "55000",
582
+ unrealizedPnl: "0",
583
+ leverage: 10,
584
+ },
585
+ ],
586
+ });
587
+ const plan = {
588
+ version: "1.0",
589
+ steps: [
590
+ { id: "buy", action: "market_order", params: { symbol: "btc", side: "buy", size: "0.1" } },
591
+ { id: "close", action: "close_position", params: { symbol: "btc" }, dependsOn: "buy" },
592
+ ],
593
+ };
594
+ await executePlan(adapter, plan);
595
+ // market_order uppercases the symbol
596
+ expect(adapter.marketOrder).toHaveBeenNthCalledWith(1, "BTC", "buy", "0.1");
597
+ // close_position uppercases for lookup and uses position's original symbol
598
+ expect(adapter.marketOrder).toHaveBeenNthCalledWith(2, "BTC", "sell", "1");
599
+ });
600
+ });
@@ -0,0 +1 @@
1
+ export {};