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,539 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ // ══════════════════════════════════════════════
3
+ // Strategy Calculators — Pure Logic Tests
4
+ //
5
+ // These tests focus on the calculation logic in the strategy modules,
6
+ // not the I/O-heavy run loops. We test:
7
+ // - TWAP: slice sizing, interval computation, last-slice remainder
8
+ // - Grid: validation, grid line generation, step calculation
9
+ // - DCA: parameter handling, state initialization
10
+ // ══════════════════════════════════════════════
11
+ // We import types and test the computational parts by exercising
12
+ // the functions with mocked adapters, using short timeouts.
13
+ // Mock jobs module to prevent file I/O
14
+ vi.mock("../jobs.js", () => ({
15
+ updateJobState: vi.fn(),
16
+ }));
17
+ // ── Mock adapter factory ──
18
+ function mockAdapter(overrides) {
19
+ return {
20
+ name: "test",
21
+ getMarkets: vi.fn().mockResolvedValue([
22
+ { symbol: "BTC", markPrice: "60000", indexPrice: "60000", fundingRate: "0.0001", volume24h: "1000000", openInterest: "500000", maxLeverage: 20 },
23
+ ]),
24
+ getBalance: vi.fn().mockResolvedValue({ equity: "10000", available: "8000", marginUsed: "2000", unrealizedPnl: "0" }),
25
+ getPositions: vi.fn().mockResolvedValue([]),
26
+ getOpenOrders: vi.fn().mockResolvedValue([]),
27
+ getOrderbook: vi.fn().mockResolvedValue({ bids: [["59990", "1"]], asks: [["60010", "1"]] }),
28
+ getOrderHistory: vi.fn().mockResolvedValue([]),
29
+ getTradeHistory: vi.fn().mockResolvedValue([]),
30
+ getRecentTrades: vi.fn().mockResolvedValue([]),
31
+ getFundingHistory: vi.fn().mockResolvedValue([]),
32
+ getFundingPayments: vi.fn().mockResolvedValue([]),
33
+ getKlines: vi.fn().mockResolvedValue([]),
34
+ marketOrder: vi.fn().mockResolvedValue({ orderId: "m1", price: "60000" }),
35
+ limitOrder: vi.fn().mockResolvedValue({ orderId: "l1" }),
36
+ editOrder: vi.fn().mockResolvedValue({}),
37
+ cancelOrder: vi.fn().mockResolvedValue({}),
38
+ cancelAllOrders: vi.fn().mockResolvedValue({}),
39
+ setLeverage: vi.fn().mockResolvedValue({}),
40
+ stopOrder: vi.fn().mockResolvedValue({}),
41
+ ...overrides,
42
+ };
43
+ }
44
+ // ══════════════════════════════════════════════
45
+ // TWAP Strategy Tests
46
+ // ══════════════════════════════════════════════
47
+ describe("TWAP — runTWAP", () => {
48
+ it("computes correct totalSlices from duration (default: 1 slice per 30s)", async () => {
49
+ const { runTWAP } = await import("../strategies/twap.js");
50
+ const adapter = mockAdapter();
51
+ const log = vi.fn();
52
+ const result = await runTWAP(adapter, {
53
+ symbol: "BTC",
54
+ side: "buy",
55
+ totalSize: 1.0,
56
+ durationSec: 3, // 3 seconds / 30 = 0.1 → Math.max(floor, 2) = 2 slices
57
+ }, undefined, log);
58
+ expect(result.totalSlices).toBe(2);
59
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(2);
60
+ });
61
+ it("uses custom slice count when provided", async () => {
62
+ const { runTWAP } = await import("../strategies/twap.js");
63
+ const adapter = mockAdapter();
64
+ const log = vi.fn();
65
+ const result = await runTWAP(adapter, {
66
+ symbol: "BTC",
67
+ side: "sell",
68
+ totalSize: 0.5,
69
+ durationSec: 5,
70
+ slices: 5,
71
+ }, undefined, log);
72
+ expect(result.totalSlices).toBe(5);
73
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(5);
74
+ });
75
+ it("fills the exact total size across all slices (no dust)", async () => {
76
+ const { runTWAP } = await import("../strategies/twap.js");
77
+ const adapter = mockAdapter();
78
+ const log = vi.fn();
79
+ const result = await runTWAP(adapter, {
80
+ symbol: "BTC",
81
+ side: "buy",
82
+ totalSize: 1.0,
83
+ durationSec: 1,
84
+ slices: 3,
85
+ }, undefined, log);
86
+ expect(result.filled).toBeCloseTo(1.0);
87
+ });
88
+ it("last slice handles remainder correctly", async () => {
89
+ const { runTWAP } = await import("../strategies/twap.js");
90
+ const adapter = mockAdapter();
91
+ const log = vi.fn();
92
+ // 1.0 / 3 = 0.3333... per slice. Last slice should do remaining.
93
+ await runTWAP(adapter, {
94
+ symbol: "BTC",
95
+ side: "buy",
96
+ totalSize: 1.0,
97
+ durationSec: 1,
98
+ slices: 3,
99
+ }, undefined, log);
100
+ const calls = adapter.marketOrder.mock.calls;
101
+ expect(calls).toHaveLength(3);
102
+ // First two slices: 1/3 = 0.3333...
103
+ const slice1 = parseFloat(calls[0][2]);
104
+ const slice2 = parseFloat(calls[1][2]);
105
+ const slice3 = parseFloat(calls[2][2]);
106
+ expect(slice1).toBeCloseTo(1 / 3, 4);
107
+ expect(slice2).toBeCloseTo(1 / 3, 4);
108
+ // Last slice = remainder (handles dust)
109
+ expect(slice1 + slice2 + slice3).toBeCloseTo(1.0, 6);
110
+ });
111
+ it("handles order errors without aborting (continues other slices)", async () => {
112
+ const { runTWAP } = await import("../strategies/twap.js");
113
+ let callCount = 0;
114
+ const adapter = mockAdapter({
115
+ marketOrder: vi.fn().mockImplementation(() => {
116
+ callCount++;
117
+ if (callCount === 2)
118
+ return Promise.reject(new Error("temporary error"));
119
+ return Promise.resolve({ orderId: "ok", price: "60000" });
120
+ }),
121
+ });
122
+ const log = vi.fn();
123
+ const result = await runTWAP(adapter, {
124
+ symbol: "BTC",
125
+ side: "buy",
126
+ totalSize: 1.0,
127
+ durationSec: 1,
128
+ slices: 4,
129
+ }, undefined, log);
130
+ expect(result.errors).toBe(1);
131
+ expect(result.slicesDone).toBe(3); // 4 attempts, 1 failed, 3 succeeded
132
+ });
133
+ it("aborts when too many errors (>50% of slices)", async () => {
134
+ const { runTWAP } = await import("../strategies/twap.js");
135
+ const adapter = mockAdapter({
136
+ marketOrder: vi.fn().mockRejectedValue(new Error("always fails")),
137
+ });
138
+ const log = vi.fn();
139
+ const result = await runTWAP(adapter, {
140
+ symbol: "BTC",
141
+ side: "buy",
142
+ totalSize: 1.0,
143
+ durationSec: 1,
144
+ slices: 4,
145
+ }, undefined, log);
146
+ // Should abort after 3rd error (3 > 4*0.5)
147
+ expect(result.errors).toBeGreaterThan(0);
148
+ expect(result.slicesDone).toBe(0);
149
+ expect(log).toHaveBeenCalledWith(expect.stringContaining("Too many errors"));
150
+ });
151
+ it("passes correct side to market orders", async () => {
152
+ const { runTWAP } = await import("../strategies/twap.js");
153
+ const adapter = mockAdapter();
154
+ const log = vi.fn();
155
+ await runTWAP(adapter, {
156
+ symbol: "ETH",
157
+ side: "sell",
158
+ totalSize: 2.0,
159
+ durationSec: 1,
160
+ slices: 2,
161
+ }, undefined, log);
162
+ for (const call of adapter.marketOrder.mock.calls) {
163
+ expect(call[0]).toBe("ETH");
164
+ expect(call[1]).toBe("sell");
165
+ }
166
+ });
167
+ });
168
+ // ══════════════════════════════════════════════
169
+ // Grid Strategy Tests
170
+ // ══════════════════════════════════════════════
171
+ describe("Grid — runGrid validation", () => {
172
+ it("rejects upperPrice <= lowerPrice", async () => {
173
+ const { runGrid } = await import("../strategies/grid.js");
174
+ const adapter = mockAdapter();
175
+ await expect(runGrid(adapter, {
176
+ symbol: "BTC",
177
+ side: "neutral",
178
+ upperPrice: 100,
179
+ lowerPrice: 100,
180
+ grids: 5,
181
+ totalSize: 1,
182
+ })).rejects.toThrow("upperPrice must be > lowerPrice");
183
+ });
184
+ it("rejects fewer than 2 grid lines", async () => {
185
+ const { runGrid } = await import("../strategies/grid.js");
186
+ const adapter = mockAdapter();
187
+ await expect(runGrid(adapter, {
188
+ symbol: "BTC",
189
+ side: "neutral",
190
+ upperPrice: 200,
191
+ lowerPrice: 100,
192
+ grids: 1,
193
+ totalSize: 1,
194
+ })).rejects.toThrow("at least 2 grid lines");
195
+ });
196
+ });
197
+ describe("Grid — order placement", () => {
198
+ it("places correct number of grid orders", async () => {
199
+ const { runGrid } = await import("../strategies/grid.js");
200
+ const adapter = mockAdapter({
201
+ // Return no open orders to prevent fill-monitoring from running
202
+ getOpenOrders: vi.fn().mockResolvedValue(
203
+ // Return all grid order IDs as still open to prevent fill loop churn
204
+ Array.from({ length: 5 }, (_, i) => ({
205
+ orderId: "l1",
206
+ symbol: "BTC",
207
+ side: "buy",
208
+ price: String(59000 + i * 500),
209
+ size: "0.2",
210
+ filled: "0",
211
+ status: "open",
212
+ type: "limit",
213
+ }))),
214
+ });
215
+ const log = vi.fn();
216
+ // Use maxRuntime to stop quickly
217
+ const resultPromise = runGrid(adapter, {
218
+ symbol: "BTC",
219
+ side: "neutral",
220
+ upperPrice: 61000,
221
+ lowerPrice: 59000,
222
+ grids: 5,
223
+ totalSize: 1,
224
+ intervalSec: 0.01,
225
+ maxRuntime: 0.05, // stop after 50ms
226
+ }, undefined, log);
227
+ const result = await resultPromise;
228
+ // Should have placed 5 initial grid orders
229
+ expect(adapter.limitOrder).toHaveBeenCalledTimes(5);
230
+ });
231
+ it("computes correct step size", async () => {
232
+ const { runGrid } = await import("../strategies/grid.js");
233
+ const adapter = mockAdapter({
234
+ getOpenOrders: vi.fn().mockResolvedValue(Array.from({ length: 3 }, () => ({
235
+ orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.1",
236
+ filled: "0", status: "open", type: "limit",
237
+ }))),
238
+ });
239
+ const log = vi.fn();
240
+ await runGrid(adapter, {
241
+ symbol: "BTC",
242
+ side: "neutral",
243
+ upperPrice: 62000,
244
+ lowerPrice: 60000,
245
+ grids: 3,
246
+ totalSize: 0.3,
247
+ intervalSec: 0.01,
248
+ maxRuntime: 0.05,
249
+ }, undefined, log);
250
+ // step = (62000 - 60000) / (3-1) = 1000
251
+ // Grid lines at: 60000, 61000, 62000
252
+ const calls = adapter.limitOrder.mock.calls;
253
+ expect(calls).toHaveLength(3);
254
+ const prices = calls.map((c) => parseFloat(c[2]));
255
+ prices.sort((a, b) => a - b);
256
+ expect(prices[0]).toBeCloseTo(60000);
257
+ expect(prices[1]).toBeCloseTo(61000);
258
+ expect(prices[2]).toBeCloseTo(62000);
259
+ });
260
+ it("distributes size equally across grid lines", async () => {
261
+ const { runGrid } = await import("../strategies/grid.js");
262
+ const adapter = mockAdapter({
263
+ getOpenOrders: vi.fn().mockResolvedValue(Array.from({ length: 4 }, () => ({
264
+ orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.25",
265
+ filled: "0", status: "open", type: "limit",
266
+ }))),
267
+ });
268
+ const log = vi.fn();
269
+ await runGrid(adapter, {
270
+ symbol: "BTC",
271
+ side: "neutral",
272
+ upperPrice: 62000,
273
+ lowerPrice: 60000,
274
+ grids: 4,
275
+ totalSize: 2.0,
276
+ intervalSec: 0.01,
277
+ maxRuntime: 0.05,
278
+ }, undefined, log);
279
+ // Each grid line gets 2.0/4 = 0.5
280
+ for (const call of adapter.limitOrder.mock.calls) {
281
+ expect(parseFloat(call[3])).toBeCloseTo(0.5);
282
+ }
283
+ });
284
+ it("assigns buy below current price and sell above for neutral side", async () => {
285
+ const { runGrid } = await import("../strategies/grid.js");
286
+ const adapter = mockAdapter({
287
+ getMarkets: vi.fn().mockResolvedValue([
288
+ { symbol: "BTC", markPrice: "61000", indexPrice: "61000", fundingRate: "0", volume24h: "0", openInterest: "0", maxLeverage: 20 },
289
+ ]),
290
+ getOpenOrders: vi.fn().mockResolvedValue(Array.from({ length: 5 }, () => ({
291
+ orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.1",
292
+ filled: "0", status: "open", type: "limit",
293
+ }))),
294
+ });
295
+ const log = vi.fn();
296
+ await runGrid(adapter, {
297
+ symbol: "BTC",
298
+ side: "neutral",
299
+ upperPrice: 63000,
300
+ lowerPrice: 59000,
301
+ grids: 5, // step = 1000. Lines: 59000, 60000, 61000, 62000, 63000
302
+ totalSize: 0.5,
303
+ intervalSec: 0.01,
304
+ maxRuntime: 0.05,
305
+ }, undefined, log);
306
+ // Current price = 61000
307
+ // 59000 → buy, 60000 → buy, 61000 → sell (>=61000), 62000 → sell, 63000 → sell
308
+ const calls = adapter.limitOrder.mock.calls;
309
+ const orders = calls.map((c) => ({ side: c[1], price: parseFloat(c[2]) }));
310
+ orders.sort((a, b) => a.price - b.price);
311
+ // Lines below current (59000, 60000) should be buy
312
+ expect(orders[0].side).toBe("buy");
313
+ expect(orders[1].side).toBe("buy");
314
+ // Lines at or above current (61000, 62000, 63000) should be sell
315
+ expect(orders[2].side).toBe("sell");
316
+ expect(orders[3].side).toBe("sell");
317
+ expect(orders[4].side).toBe("sell");
318
+ });
319
+ it("sets leverage if provided", async () => {
320
+ const { runGrid } = await import("../strategies/grid.js");
321
+ const adapter = mockAdapter({
322
+ getOpenOrders: vi.fn().mockResolvedValue([
323
+ { orderId: "l1", symbol: "BTC", side: "buy", price: "60000", size: "0.5", filled: "0", status: "open", type: "limit" },
324
+ { orderId: "l1", symbol: "BTC", side: "sell", price: "62000", size: "0.5", filled: "0", status: "open", type: "limit" },
325
+ ]),
326
+ });
327
+ const log = vi.fn();
328
+ await runGrid(adapter, {
329
+ symbol: "BTC",
330
+ side: "neutral",
331
+ upperPrice: 62000,
332
+ lowerPrice: 60000,
333
+ grids: 2,
334
+ totalSize: 1,
335
+ leverage: 5,
336
+ intervalSec: 0.01,
337
+ maxRuntime: 0.05,
338
+ }, undefined, log);
339
+ expect(adapter.setLeverage).toHaveBeenCalledWith("BTC", 5);
340
+ });
341
+ });
342
+ // ══════════════════════════════════════════════
343
+ // DCA Strategy Tests
344
+ // ══════════════════════════════════════════════
345
+ describe("DCA — runDCA", () => {
346
+ it("places the specified number of orders then stops", async () => {
347
+ const { runDCA } = await import("../strategies/dca.js");
348
+ const adapter = mockAdapter();
349
+ const log = vi.fn();
350
+ const result = await runDCA(adapter, {
351
+ symbol: "BTC",
352
+ side: "buy",
353
+ amountPerOrder: 0.01,
354
+ intervalSec: 0.001, // 1ms intervals for fast test
355
+ totalOrders: 3,
356
+ }, undefined, log);
357
+ expect(result.ordersPlaced).toBe(3);
358
+ expect(adapter.marketOrder).toHaveBeenCalledTimes(3);
359
+ });
360
+ it("passes correct symbol, side, and size to each order", async () => {
361
+ const { runDCA } = await import("../strategies/dca.js");
362
+ const adapter = mockAdapter();
363
+ const log = vi.fn();
364
+ await runDCA(adapter, {
365
+ symbol: "ETH",
366
+ side: "sell",
367
+ amountPerOrder: 0.5,
368
+ intervalSec: 0.001,
369
+ totalOrders: 2,
370
+ }, undefined, log);
371
+ for (const call of adapter.marketOrder.mock.calls) {
372
+ expect(call[0]).toBe("ETH");
373
+ expect(call[1]).toBe("sell");
374
+ expect(call[2]).toBe("0.5");
375
+ }
376
+ });
377
+ it("tracks total filled amount", async () => {
378
+ const { runDCA } = await import("../strategies/dca.js");
379
+ const adapter = mockAdapter();
380
+ const log = vi.fn();
381
+ const result = await runDCA(adapter, {
382
+ symbol: "BTC",
383
+ side: "buy",
384
+ amountPerOrder: 0.1,
385
+ intervalSec: 0.001,
386
+ totalOrders: 5,
387
+ }, undefined, log);
388
+ expect(result.totalFilled).toBeCloseTo(0.5);
389
+ });
390
+ it("computes average price from fill prices", async () => {
391
+ const { runDCA } = await import("../strategies/dca.js");
392
+ let callNum = 0;
393
+ const prices = [59000, 60000, 61000];
394
+ const adapter = mockAdapter({
395
+ marketOrder: vi.fn().mockImplementation(() => {
396
+ const price = prices[callNum++];
397
+ return Promise.resolve({ orderId: `m${callNum}`, price: String(price) });
398
+ }),
399
+ });
400
+ const log = vi.fn();
401
+ const result = await runDCA(adapter, {
402
+ symbol: "BTC",
403
+ side: "buy",
404
+ amountPerOrder: 1,
405
+ intervalSec: 0.001,
406
+ totalOrders: 3,
407
+ }, undefined, log);
408
+ // avg = (1*59000 + 1*60000 + 1*61000) / 3 = 60000
409
+ expect(result.avgPrice).toBeCloseTo(60000);
410
+ });
411
+ it("continues on individual order errors", async () => {
412
+ const { runDCA } = await import("../strategies/dca.js");
413
+ // DCA keeps going until ordersPlaced reaches totalOrders.
414
+ // If one call fails, it retries on the next loop iteration.
415
+ // So with totalOrders=3, it needs 4 calls: succeed, fail, succeed, succeed → 3 placed.
416
+ const marketOrderMock = vi.fn()
417
+ .mockResolvedValueOnce({ orderId: "m1", price: "60000" })
418
+ .mockRejectedValueOnce(new Error("temporary"))
419
+ .mockResolvedValueOnce({ orderId: "m3", price: "60000" })
420
+ .mockResolvedValueOnce({ orderId: "m4", price: "60000" });
421
+ const adapter = mockAdapter({ marketOrder: marketOrderMock });
422
+ const log = vi.fn();
423
+ const result = await runDCA(adapter, {
424
+ symbol: "BTC",
425
+ side: "buy",
426
+ amountPerOrder: 0.1,
427
+ intervalSec: 0.001,
428
+ totalOrders: 3,
429
+ }, undefined, log);
430
+ // 4 marketOrder calls: 3 succeeded, 1 failed
431
+ expect(marketOrderMock).toHaveBeenCalledTimes(4);
432
+ expect(result.ordersPlaced).toBe(3);
433
+ // Verify an error was logged
434
+ expect(log).toHaveBeenCalledWith(expect.stringContaining("Order error"));
435
+ });
436
+ it("stops when too many errors (errors > ordersPlaced and errors > 10)", async () => {
437
+ const { runDCA } = await import("../strategies/dca.js");
438
+ const adapter = mockAdapter({
439
+ marketOrder: vi.fn().mockRejectedValue(new Error("always fails")),
440
+ });
441
+ const log = vi.fn();
442
+ const result = await runDCA(adapter, {
443
+ symbol: "BTC",
444
+ side: "buy",
445
+ amountPerOrder: 0.1,
446
+ intervalSec: 0.001,
447
+ totalOrders: 100, // high count but errors will stop it
448
+ }, undefined, log);
449
+ // Stops at 11 errors (>10 and >0 placed)
450
+ expect(result.ordersPlaced).toBe(0);
451
+ expect(log).toHaveBeenCalledWith(expect.stringContaining("Too many errors"));
452
+ });
453
+ it("skips order when buy price exceeds price limit", async () => {
454
+ const { runDCA } = await import("../strategies/dca.js");
455
+ const adapter = mockAdapter({
456
+ getMarkets: vi.fn().mockResolvedValue([
457
+ { symbol: "BTC", markPrice: "65000", indexPrice: "65000", fundingRate: "0", volume24h: "0", openInterest: "0", maxLeverage: 20 },
458
+ ]),
459
+ });
460
+ const log = vi.fn();
461
+ // Price limit of $60000, but market is at $65000 — should skip
462
+ // With totalOrders=0 this would loop forever, so we use maxRuntime
463
+ const result = await runDCA(adapter, {
464
+ symbol: "BTC",
465
+ side: "buy",
466
+ amountPerOrder: 0.1,
467
+ intervalSec: 0.001,
468
+ totalOrders: 2,
469
+ priceLimit: 60000,
470
+ maxRuntime: 0.1, // 100ms timeout
471
+ }, undefined, log);
472
+ expect(result.ordersPlaced).toBe(0);
473
+ expect(adapter.marketOrder).not.toHaveBeenCalled();
474
+ });
475
+ it("respects maxRuntime", async () => {
476
+ const { runDCA } = await import("../strategies/dca.js");
477
+ const adapter = mockAdapter();
478
+ const log = vi.fn();
479
+ const start = Date.now();
480
+ const result = await runDCA(adapter, {
481
+ symbol: "BTC",
482
+ side: "buy",
483
+ amountPerOrder: 0.01,
484
+ intervalSec: 0.01, // 10ms intervals (short enough to not block)
485
+ totalOrders: 0, // unlimited
486
+ maxRuntime: 0.1, // 100ms max
487
+ }, undefined, log);
488
+ const elapsed = Date.now() - start;
489
+ // Should stop within a reasonable time (definitely under 5s)
490
+ expect(elapsed).toBeLessThan(5000);
491
+ // With 10ms intervals and 100ms runtime, should place a handful of orders but not many
492
+ expect(result.ordersPlaced).toBeGreaterThanOrEqual(1);
493
+ expect(result.ordersPlaced).toBeLessThan(50); // sanity check
494
+ });
495
+ });
496
+ // ══════════════════════════════════════════════
497
+ // TWAP Slice Calculation (unit-level)
498
+ // ══════════════════════════════════════════════
499
+ describe("TWAP — slice calculation edge cases", () => {
500
+ it("minimum 2 slices even for very short duration", async () => {
501
+ const { runTWAP } = await import("../strategies/twap.js");
502
+ const adapter = mockAdapter();
503
+ const log = vi.fn();
504
+ const result = await runTWAP(adapter, {
505
+ symbol: "BTC",
506
+ side: "buy",
507
+ totalSize: 0.1,
508
+ durationSec: 1, // 1s / 30 = 0.03 → floor = 0 → max(0, 2) = 2
509
+ }, undefined, log);
510
+ expect(result.totalSlices).toBe(2);
511
+ });
512
+ it("reports correct remaining after partial completion", async () => {
513
+ const { runTWAP } = await import("../strategies/twap.js");
514
+ const marketOrderMock = vi.fn()
515
+ .mockResolvedValueOnce({ orderId: "ok" })
516
+ .mockRejectedValueOnce(new Error("fail"))
517
+ .mockResolvedValueOnce({ orderId: "ok" });
518
+ const adapter = mockAdapter({ marketOrder: marketOrderMock });
519
+ const log = vi.fn();
520
+ const result = await runTWAP(adapter, {
521
+ symbol: "BTC",
522
+ side: "buy",
523
+ totalSize: 1.0,
524
+ durationSec: 1,
525
+ slices: 3,
526
+ }, undefined, log);
527
+ // 3 slices, each nominally 1/3. Slice 2 fails.
528
+ // Slice 1: filled += 1/3, remaining = 2/3
529
+ // Slice 2: fails, no change to filled/remaining
530
+ // Slice 3 is last slice: uses state.remaining = 2/3 as the size
531
+ // Slice 3: filled += 2/3, remaining = 0
532
+ // So final remaining = 0, filled = 1.0
533
+ expect(result.slicesDone).toBe(2);
534
+ expect(result.errors).toBe(1);
535
+ // Last slice picks up all remaining, so total filled = 1/3 + 2/3 = 1.0
536
+ expect(result.remaining).toBeCloseTo(0, 2);
537
+ expect(result.filled).toBeCloseTo(1.0, 2);
538
+ });
539
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { MockAdapter, createMockPositions } from "./exchanges/mock-adapter.js";
3
+ describe("Trade Execution", () => {
4
+ let adapter;
5
+ beforeEach(() => {
6
+ adapter = new MockAdapter("test-exchange");
7
+ adapter.orderResult = { status: "ok", orderId: "test-123" };
8
+ });
9
+ describe("Market Orders", () => {
10
+ it("should place a buy market order with correct params", async () => {
11
+ await adapter.marketOrder("BTC", "buy", "0.1");
12
+ const calls = adapter.getCallsFor("marketOrder");
13
+ expect(calls).toHaveLength(1);
14
+ expect(calls[0].args).toEqual(["BTC", "buy", "0.1"]);
15
+ });
16
+ it("should place a sell market order", async () => {
17
+ await adapter.marketOrder("ETH", "sell", "1.5");
18
+ const calls = adapter.getCallsFor("marketOrder");
19
+ expect(calls).toHaveLength(1);
20
+ expect(calls[0].args).toEqual(["ETH", "sell", "1.5"]);
21
+ });
22
+ it("should return order result", async () => {
23
+ const result = await adapter.marketOrder("BTC", "buy", "0.1");
24
+ expect(result).toEqual({ status: "ok", orderId: "test-123" });
25
+ });
26
+ });
27
+ describe("Limit Orders", () => {
28
+ it("should place limit order with price and size", async () => {
29
+ await adapter.limitOrder("BTC", "buy", "95000", "0.05");
30
+ const calls = adapter.getCallsFor("limitOrder");
31
+ expect(calls).toHaveLength(1);
32
+ expect(calls[0].args).toEqual(["BTC", "buy", "95000", "0.05"]);
33
+ });
34
+ });
35
+ describe("Stop Orders", () => {
36
+ it("should place stop order with trigger price", async () => {
37
+ await adapter.stopOrder("BTC", "sell", "0.1", "90000", { reduceOnly: true });
38
+ const calls = adapter.getCallsFor("stopOrder");
39
+ expect(calls).toHaveLength(1);
40
+ expect(calls[0].args).toEqual(["BTC", "sell", "0.1", "90000", { reduceOnly: true }]);
41
+ });
42
+ it("should place stop-limit order", async () => {
43
+ await adapter.stopOrder("ETH", "buy", "1.0", "4000", { limitPrice: "4050", reduceOnly: false });
44
+ const calls = adapter.getCallsFor("stopOrder");
45
+ expect(calls[0].args[4]).toEqual({ limitPrice: "4050", reduceOnly: false });
46
+ });
47
+ });
48
+ describe("Order Management", () => {
49
+ it("should cancel specific order", async () => {
50
+ await adapter.cancelOrder("BTC", "order-456");
51
+ expect(adapter.getCallsFor("cancelOrder")[0].args).toEqual(["BTC", "order-456"]);
52
+ });
53
+ it("should cancel all orders", async () => {
54
+ await adapter.cancelAllOrders();
55
+ expect(adapter.getCallsFor("cancelAllOrders")).toHaveLength(1);
56
+ });
57
+ it("should cancel orders for specific symbol", async () => {
58
+ await adapter.cancelAllOrders("ETH");
59
+ expect(adapter.getCallsFor("cancelAllOrders")[0].args).toEqual(["ETH"]);
60
+ });
61
+ it("should edit existing order", async () => {
62
+ await adapter.editOrder("BTC", "order-789", "96000", "0.2");
63
+ expect(adapter.getCallsFor("editOrder")[0].args).toEqual(["BTC", "order-789", "96000", "0.2"]);
64
+ });
65
+ });
66
+ describe("Position Close Logic", () => {
67
+ it("should close long position with sell order", async () => {
68
+ adapter.positionsResponse = createMockPositions(1); // long BTC 0.1
69
+ const positions = await adapter.getPositions();
70
+ expect(positions).toHaveLength(1);
71
+ expect(positions[0].side).toBe("long");
72
+ // Close: sell the same size
73
+ await adapter.marketOrder(positions[0].symbol, "sell", positions[0].size);
74
+ const calls = adapter.getCallsFor("marketOrder");
75
+ expect(calls).toHaveLength(1);
76
+ expect(calls[0].args).toEqual(["BTC", "sell", "0.1"]);
77
+ });
78
+ it("should close short position with buy order", async () => {
79
+ adapter.positionsResponse = [{
80
+ symbol: "ETH", side: "short", size: "2.0",
81
+ entryPrice: "3500", markPrice: "3400", liquidationPrice: "4500",
82
+ unrealizedPnl: "200", leverage: 5,
83
+ }];
84
+ const positions = await adapter.getPositions();
85
+ await adapter.marketOrder(positions[0].symbol, "buy", positions[0].size);
86
+ const calls = adapter.getCallsFor("marketOrder");
87
+ expect(calls[0].args).toEqual(["ETH", "buy", "2.0"]);
88
+ });
89
+ it("should close all positions with opposite-side orders", async () => {
90
+ adapter.positionsResponse = [
91
+ { symbol: "BTC", side: "long", size: "0.1", entryPrice: "100000", markPrice: "101000", liquidationPrice: "90000", unrealizedPnl: "100", leverage: 10 },
92
+ { symbol: "ETH", side: "short", size: "1.5", entryPrice: "3500", markPrice: "3400", liquidationPrice: "4500", unrealizedPnl: "150", leverage: 5 },
93
+ ];
94
+ const positions = await adapter.getPositions();
95
+ for (const pos of positions) {
96
+ const closeSide = pos.side === "long" ? "sell" : "buy";
97
+ await adapter.marketOrder(pos.symbol, closeSide, pos.size);
98
+ }
99
+ const calls = adapter.getCallsFor("marketOrder");
100
+ expect(calls).toHaveLength(2);
101
+ expect(calls[0].args).toEqual(["BTC", "sell", "0.1"]);
102
+ expect(calls[1].args).toEqual(["ETH", "buy", "1.5"]);
103
+ });
104
+ it("should reduce position by percentage", async () => {
105
+ adapter.positionsResponse = [{
106
+ symbol: "BTC", side: "long", size: "1.0",
107
+ entryPrice: "100000", markPrice: "101000", liquidationPrice: "90000",
108
+ unrealizedPnl: "1000", leverage: 10,
109
+ }];
110
+ const positions = await adapter.getPositions();
111
+ const pos = positions[0];
112
+ const reducePct = 50;
113
+ const reduceSize = (Number(pos.size) * reducePct / 100).toString();
114
+ await adapter.marketOrder(pos.symbol, "sell", reduceSize);
115
+ const calls = adapter.getCallsFor("marketOrder");
116
+ expect(calls[0].args).toEqual(["BTC", "sell", "0.5"]);
117
+ });
118
+ });
119
+ describe("Leverage Management", () => {
120
+ it("should set leverage with cross margin", async () => {
121
+ await adapter.setLeverage("BTC", 20, "cross");
122
+ expect(adapter.getCallsFor("setLeverage")[0].args).toEqual(["BTC", 20, "cross"]);
123
+ });
124
+ it("should set leverage with isolated margin", async () => {
125
+ await adapter.setLeverage("ETH", 5, "isolated");
126
+ expect(adapter.getCallsFor("setLeverage")[0].args).toEqual(["ETH", 5, "isolated"]);
127
+ });
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};