perp-cli 0.11.0 → 0.12.0

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