perp-cli 0.9.8 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
@@ -1 +0,0 @@
1
- export {};
@@ -1,310 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { withRetry, withRetrySimple, wrapAdapterWithRetry, applyJitter, computeDelay, RetriesExhaustedError, } from "../retry.js";
3
- // ── Helpers ──
4
- /** Create an error whose message triggers classification as the given code */
5
- function rateLimitError() {
6
- return new Error("429 Too Many Requests");
7
- }
8
- function networkError() {
9
- return new Error("fetch failed");
10
- }
11
- function timeoutError() {
12
- return new Error("Request timed out");
13
- }
14
- function insufficientBalanceError() {
15
- return new Error("Insufficient balance for order");
16
- }
17
- // Speed up tests by using tiny delays
18
- const FAST_OPTS = {
19
- maxRetries: 3,
20
- baseDelayMs: 1,
21
- maxDelayMs: 10,
22
- backoffMultiplier: 2,
23
- };
24
- describe("withRetry", () => {
25
- beforeEach(() => {
26
- vi.spyOn(Math, "random").mockReturnValue(0.5); // jitter factor = 0.8 + 0.5*0.4 = 1.0
27
- });
28
- afterEach(() => {
29
- vi.restoreAllMocks();
30
- });
31
- it("returns result on first successful attempt (no retry)", async () => {
32
- const fn = vi.fn().mockResolvedValue("ok");
33
- const result = await withRetry(fn, FAST_OPTS);
34
- expect(result.data).toBe("ok");
35
- expect(result.attempts).toBe(1);
36
- expect(result.totalDelayMs).toBe(0);
37
- expect(result.retries).toHaveLength(0);
38
- expect(fn).toHaveBeenCalledTimes(1);
39
- });
40
- it("retries on retryable error (rate limit) and succeeds", async () => {
41
- const fn = vi.fn()
42
- .mockRejectedValueOnce(rateLimitError())
43
- .mockRejectedValueOnce(networkError())
44
- .mockResolvedValue("recovered");
45
- const result = await withRetry(fn, FAST_OPTS);
46
- expect(result.data).toBe("recovered");
47
- expect(result.attempts).toBe(3);
48
- expect(result.retries).toHaveLength(2);
49
- expect(result.retries[0].error.code).toBe("RATE_LIMITED");
50
- expect(result.retries[1].error.code).toBe("EXCHANGE_UNREACHABLE");
51
- expect(fn).toHaveBeenCalledTimes(3);
52
- });
53
- it("throws immediately on non-retryable error (insufficient balance)", async () => {
54
- const fn = vi.fn().mockRejectedValue(insufficientBalanceError());
55
- await expect(withRetry(fn, FAST_OPTS)).rejects.toThrow("Insufficient balance");
56
- expect(fn).toHaveBeenCalledTimes(1);
57
- });
58
- it("throws RetriesExhaustedError when max retries exceeded", async () => {
59
- const fn = vi.fn().mockRejectedValue(rateLimitError());
60
- try {
61
- await withRetry(fn, { ...FAST_OPTS, maxRetries: 2 });
62
- expect.fail("Should have thrown");
63
- }
64
- catch (err) {
65
- expect(err).toBeInstanceOf(RetriesExhaustedError);
66
- const rex = err;
67
- expect(rex.attempts).toBe(3); // 1 initial + 2 retries
68
- expect(rex.lastError.code).toBe("RATE_LIMITED");
69
- expect(rex.retries).toHaveLength(2);
70
- }
71
- // initial attempt + 2 retries + 1 final attempt = 3 calls
72
- expect(fn).toHaveBeenCalledTimes(3);
73
- });
74
- it("uses exponential backoff timing", async () => {
75
- // With Math.random() = 0.5, jitter factor = 1.0 (no change)
76
- const fn = vi.fn()
77
- .mockRejectedValueOnce(networkError())
78
- .mockRejectedValueOnce(networkError())
79
- .mockRejectedValueOnce(networkError())
80
- .mockResolvedValue("ok");
81
- const opts = {
82
- maxRetries: 3,
83
- baseDelayMs: 100,
84
- maxDelayMs: 10000,
85
- backoffMultiplier: 2,
86
- };
87
- const result = await withRetry(fn, opts);
88
- // Attempt 1 fail: delay = 100 * 2^0 = 100
89
- // Attempt 2 fail: delay = 100 * 2^1 = 200
90
- // Attempt 3 fail: delay = 100 * 2^2 = 400
91
- expect(result.retries[0].delayMs).toBe(100);
92
- expect(result.retries[1].delayMs).toBe(200);
93
- expect(result.retries[2].delayMs).toBe(400);
94
- expect(result.totalDelayMs).toBe(700);
95
- });
96
- it("respects retryAfterMs from error code (rate limit = 1000ms)", async () => {
97
- // Rate limit has retryAfterMs: 1000
98
- // With baseDelayMs=1 and multiplier=2, backoff would be tiny,
99
- // but retryAfterMs: 1000 should override
100
- const fn = vi.fn()
101
- .mockRejectedValueOnce(rateLimitError())
102
- .mockResolvedValue("ok");
103
- const opts = {
104
- maxRetries: 3,
105
- baseDelayMs: 1,
106
- maxDelayMs: 30000,
107
- backoffMultiplier: 2,
108
- };
109
- const result = await withRetry(fn, opts);
110
- // retryAfterMs=1000 is larger than baseDelay*multiplier^0=1, so 1000 is used
111
- expect(result.retries[0].delayMs).toBe(1000);
112
- });
113
- it("caps delay at maxDelayMs", async () => {
114
- const fn = vi.fn()
115
- .mockRejectedValueOnce(networkError())
116
- .mockResolvedValue("ok");
117
- const opts = {
118
- maxRetries: 3,
119
- baseDelayMs: 50000, // Very large base
120
- maxDelayMs: 100,
121
- backoffMultiplier: 2,
122
- };
123
- const result = await withRetry(fn, opts);
124
- // 50000 would be the computed delay, but capped to 100
125
- expect(result.retries[0].delayMs).toBe(100);
126
- });
127
- it("calls onRetry callback on each retry", async () => {
128
- const onRetry = vi.fn();
129
- const fn = vi.fn()
130
- .mockRejectedValueOnce(rateLimitError())
131
- .mockRejectedValueOnce(timeoutError())
132
- .mockResolvedValue("ok");
133
- await withRetry(fn, { ...FAST_OPTS, onRetry });
134
- expect(onRetry).toHaveBeenCalledTimes(2);
135
- // First call: attempt 1, rate limit error
136
- expect(onRetry.mock.calls[0][0]).toBe(1); // attempt
137
- expect(onRetry.mock.calls[0][1].code).toBe("RATE_LIMITED");
138
- expect(typeof onRetry.mock.calls[0][2]).toBe("number"); // delayMs
139
- // Second call: attempt 2, timeout error
140
- expect(onRetry.mock.calls[1][0]).toBe(2);
141
- expect(onRetry.mock.calls[1][1].code).toBe("TIMEOUT");
142
- });
143
- it("uses default options when none provided", async () => {
144
- // Just verify it runs with defaults (maxRetries=3, baseDelay=1000, etc.)
145
- const fn = vi.fn().mockResolvedValue(42);
146
- const result = await withRetry(fn);
147
- expect(result.data).toBe(42);
148
- expect(result.attempts).toBe(1);
149
- });
150
- });
151
- describe("applyJitter", () => {
152
- afterEach(() => {
153
- vi.restoreAllMocks();
154
- });
155
- it("applies jitter within +/-20% range", () => {
156
- // Test minimum jitter (random=0 -> factor=0.8)
157
- vi.spyOn(Math, "random").mockReturnValue(0);
158
- expect(applyJitter(1000)).toBe(800);
159
- // Test maximum jitter (random=1 -> factor=1.2)
160
- vi.mocked(Math.random).mockReturnValue(1);
161
- expect(applyJitter(1000)).toBe(1200);
162
- // Test midpoint (random=0.5 -> factor=1.0)
163
- vi.mocked(Math.random).mockReturnValue(0.5);
164
- expect(applyJitter(1000)).toBe(1000);
165
- });
166
- it("rounds to integer", () => {
167
- vi.spyOn(Math, "random").mockReturnValue(0.3); // factor = 0.92
168
- expect(applyJitter(100)).toBe(92);
169
- expect(Number.isInteger(applyJitter(100))).toBe(true);
170
- });
171
- });
172
- describe("computeDelay", () => {
173
- beforeEach(() => {
174
- vi.spyOn(Math, "random").mockReturnValue(0.5); // jitter factor = 1.0
175
- });
176
- afterEach(() => {
177
- vi.restoreAllMocks();
178
- });
179
- const defaultOpts = {
180
- maxRetries: 3,
181
- baseDelayMs: 1000,
182
- maxDelayMs: 30000,
183
- backoffMultiplier: 2,
184
- };
185
- it("computes exponential backoff", () => {
186
- const error = { code: "TIMEOUT", message: "timed out", status: 504, retryable: true };
187
- expect(computeDelay(1, error, defaultOpts)).toBe(1000); // 1000 * 2^0
188
- expect(computeDelay(2, error, defaultOpts)).toBe(2000); // 1000 * 2^1
189
- expect(computeDelay(3, error, defaultOpts)).toBe(4000); // 1000 * 2^2
190
- });
191
- it("uses retryAfterMs when larger than backoff", () => {
192
- const error = {
193
- code: "RATE_LIMITED", message: "429", status: 429, retryable: true, retryAfterMs: 5000,
194
- };
195
- // Attempt 1: backoff = 1000, retryAfterMs = 5000 -> 5000
196
- expect(computeDelay(1, error, defaultOpts)).toBe(5000);
197
- // Attempt 3: backoff = 4000, retryAfterMs = 5000 -> 5000
198
- expect(computeDelay(3, error, defaultOpts)).toBe(5000);
199
- // Attempt 4: backoff = 8000, retryAfterMs = 5000 -> 8000
200
- expect(computeDelay(4, error, defaultOpts)).toBe(8000);
201
- });
202
- it("caps at maxDelayMs", () => {
203
- const error = { code: "TIMEOUT", message: "timed out", status: 504, retryable: true };
204
- const opts = { ...defaultOpts, maxDelayMs: 3000 };
205
- expect(computeDelay(3, error, opts)).toBe(3000); // 4000 capped to 3000
206
- });
207
- });
208
- describe("withRetrySimple", () => {
209
- beforeEach(() => {
210
- vi.spyOn(Math, "random").mockReturnValue(0.5);
211
- });
212
- afterEach(() => {
213
- vi.restoreAllMocks();
214
- });
215
- it("returns data directly without metadata", async () => {
216
- const fn = vi.fn()
217
- .mockRejectedValueOnce(rateLimitError())
218
- .mockResolvedValue({ price: "100.00" });
219
- const data = await withRetrySimple(fn, 3);
220
- expect(data).toEqual({ price: "100.00" });
221
- });
222
- it("throws on non-retryable error", async () => {
223
- const fn = vi.fn().mockRejectedValue(insufficientBalanceError());
224
- await expect(withRetrySimple(fn)).rejects.toThrow("Insufficient balance");
225
- });
226
- it("uses default maxRetries when not specified", async () => {
227
- const fn = vi.fn().mockResolvedValue("ok");
228
- const result = await withRetrySimple(fn);
229
- expect(result).toBe("ok");
230
- });
231
- });
232
- describe("wrapAdapterWithRetry", () => {
233
- beforeEach(() => {
234
- vi.spyOn(Math, "random").mockReturnValue(0.5);
235
- });
236
- afterEach(() => {
237
- vi.restoreAllMocks();
238
- });
239
- function createMockAdapter(overrides) {
240
- return {
241
- name: "mock-exchange",
242
- getMarkets: vi.fn().mockResolvedValue([]),
243
- getOrderbook: vi.fn().mockResolvedValue({ bids: [], asks: [] }),
244
- getRecentTrades: vi.fn().mockResolvedValue([]),
245
- getFundingHistory: vi.fn().mockResolvedValue([]),
246
- getKlines: vi.fn().mockResolvedValue([]),
247
- getBalance: vi.fn().mockResolvedValue({ equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" }),
248
- getPositions: vi.fn().mockResolvedValue([]),
249
- getOpenOrders: vi.fn().mockResolvedValue([]),
250
- getOrderHistory: vi.fn().mockResolvedValue([]),
251
- getTradeHistory: vi.fn().mockResolvedValue([]),
252
- getFundingPayments: vi.fn().mockResolvedValue([]),
253
- marketOrder: vi.fn().mockResolvedValue({ orderId: "123" }),
254
- limitOrder: vi.fn().mockResolvedValue({ orderId: "456" }),
255
- editOrder: vi.fn().mockResolvedValue({}),
256
- cancelOrder: vi.fn().mockResolvedValue({}),
257
- cancelAllOrders: vi.fn().mockResolvedValue({}),
258
- setLeverage: vi.fn().mockResolvedValue({}),
259
- stopOrder: vi.fn().mockResolvedValue({}),
260
- ...overrides,
261
- };
262
- }
263
- it("passes through the name property without wrapping", () => {
264
- const adapter = createMockAdapter();
265
- const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
266
- expect(wrapped.name).toBe("mock-exchange");
267
- });
268
- it("proxies async method calls through to the original adapter", async () => {
269
- const mockMarkets = [{ symbol: "BTC-PERP", markPrice: "50000" }];
270
- const adapter = createMockAdapter({
271
- getMarkets: vi.fn().mockResolvedValue(mockMarkets),
272
- });
273
- const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
274
- const result = await wrapped.getMarkets();
275
- expect(result).toEqual(mockMarkets);
276
- expect(adapter.getMarkets).toHaveBeenCalledTimes(1);
277
- });
278
- it("retries async methods on retryable errors", async () => {
279
- const getBalance = vi.fn()
280
- .mockRejectedValueOnce(rateLimitError())
281
- .mockResolvedValue({ equity: "1000", available: "500", marginUsed: "500", unrealizedPnl: "50" });
282
- const adapter = createMockAdapter({ getBalance });
283
- const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
284
- const result = await wrapped.getBalance();
285
- expect(result).toEqual({ equity: "1000", available: "500", marginUsed: "500", unrealizedPnl: "50" });
286
- expect(getBalance).toHaveBeenCalledTimes(2);
287
- });
288
- it("passes arguments through to the wrapped method", async () => {
289
- const marketOrder = vi.fn().mockResolvedValue({ orderId: "abc" });
290
- const adapter = createMockAdapter({ marketOrder });
291
- const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
292
- await wrapped.marketOrder("BTC-PERP", "buy", "0.1");
293
- expect(marketOrder).toHaveBeenCalledWith("BTC-PERP", "buy", "0.1");
294
- });
295
- it("does not retry non-retryable errors from wrapped methods", async () => {
296
- const marketOrder = vi.fn().mockRejectedValue(insufficientBalanceError());
297
- const adapter = createMockAdapter({ marketOrder });
298
- const wrapped = wrapAdapterWithRetry(adapter, FAST_OPTS);
299
- await expect(wrapped.marketOrder("BTC-PERP", "buy", "999")).rejects.toThrow("Insufficient balance");
300
- expect(marketOrder).toHaveBeenCalledTimes(1);
301
- });
302
- it("respects retry options passed to wrapAdapterWithRetry", async () => {
303
- const getPositions = vi.fn().mockRejectedValue(timeoutError());
304
- const adapter = createMockAdapter({ getPositions });
305
- const wrapped = wrapAdapterWithRetry(adapter, { ...FAST_OPTS, maxRetries: 1 });
306
- await expect(wrapped.getPositions()).rejects.toBeInstanceOf(RetriesExhaustedError);
307
- // 1 initial + 1 retry = 2 calls
308
- expect(getPositions).toHaveBeenCalledTimes(2);
309
- });
310
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,288 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { assessRisk, preTradeCheck, calcLiquidationDistance, getLiquidationDistances, effectiveLimit } from "../risk.js";
3
- const defaultLimits = {
4
- maxDrawdownUsd: 500,
5
- maxPositionUsd: 5000,
6
- maxTotalExposureUsd: 20000,
7
- dailyLossLimitUsd: 200,
8
- maxPositions: 10,
9
- maxLeverage: 20,
10
- maxMarginUtilization: 80,
11
- minLiquidationDistance: 30,
12
- };
13
- function makeBalance(equity, available, marginUsed, pnl) {
14
- return { equity: String(equity), available: String(available), marginUsed: String(marginUsed), unrealizedPnl: String(pnl) };
15
- }
16
- function makePosition(symbol, side, size, markPrice, leverage, pnl, liquidationPrice = "0") {
17
- return { symbol, side, size: String(size), entryPrice: String(markPrice), markPrice: String(markPrice), liquidationPrice, unrealizedPnl: String(pnl), leverage };
18
- }
19
- describe("Risk Assessment", () => {
20
- it("should return low risk when everything is within limits", () => {
21
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 100) }];
22
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, 50) }];
23
- const result = assessRisk(balances, positions, defaultLimits);
24
- expect(result.level).toBe("low");
25
- expect(result.violations).toHaveLength(0);
26
- expect(result.canTrade).toBe(true);
27
- });
28
- it("should detect max drawdown violation (critical)", () => {
29
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, -600) }];
30
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, -600) }];
31
- const result = assessRisk(balances, positions, defaultLimits);
32
- expect(result.level).toBe("critical");
33
- expect(result.violations.some(v => v.rule === "max_drawdown")).toBe(true);
34
- expect(result.canTrade).toBe(false);
35
- });
36
- it("should detect max position size violation", () => {
37
- const balances = [{ exchange: "test", balance: makeBalance(10000, 5000, 5000, 0) }];
38
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.1, 100000, 10, 0) }]; // 0.1 * 100000 = $10000 > $5000 limit
39
- const result = assessRisk(balances, positions, defaultLimits);
40
- expect(result.violations.some(v => v.rule === "max_position_size")).toBe(true);
41
- });
42
- it("should detect max total exposure violation", () => {
43
- const balances = [{ exchange: "test", balance: makeBalance(50000, 30000, 20000, 0) }];
44
- const positions = [
45
- { exchange: "test", position: makePosition("BTC", "long", 0.1, 100000, 5, 0) }, // $10000
46
- { exchange: "test", position: makePosition("ETH", "short", 5, 3500, 5, 0) }, // $17500
47
- ]; // total = $27500 > $20000
48
- const result = assessRisk(balances, positions, defaultLimits);
49
- expect(result.violations.some(v => v.rule === "max_total_exposure")).toBe(true);
50
- });
51
- it("should detect max positions violation", () => {
52
- const limits = { ...defaultLimits, maxPositions: 2 };
53
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
54
- const positions = [
55
- { exchange: "test", position: makePosition("BTC", "long", 0.001, 100000, 2, 0) },
56
- { exchange: "test", position: makePosition("ETH", "short", 0.01, 3500, 2, 0) },
57
- { exchange: "test", position: makePosition("SOL", "long", 0.1, 150, 2, 0) },
58
- ];
59
- const result = assessRisk(balances, positions, limits);
60
- expect(result.violations.some(v => v.rule === "max_positions")).toBe(true);
61
- });
62
- it("should detect max leverage violation", () => {
63
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
64
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 50, 0) }]; // 50x > 20x
65
- const result = assessRisk(balances, positions, defaultLimits);
66
- expect(result.violations.some(v => v.rule === "max_leverage")).toBe(true);
67
- });
68
- it("should detect margin utilization violation", () => {
69
- const balances = [{ exchange: "test", balance: makeBalance(1000, 100, 900, 0) }]; // 90% margin usage
70
- const positions = [];
71
- const result = assessRisk(balances, positions, defaultLimits);
72
- expect(result.violations.some(v => v.rule === "max_margin_utilization")).toBe(true);
73
- });
74
- it("should calculate correct metrics", () => {
75
- const balances = [
76
- { exchange: "ex1", balance: makeBalance(5000, 3000, 2000, 100) },
77
- { exchange: "ex2", balance: makeBalance(3000, 2000, 1000, -50) },
78
- ];
79
- const positions = [
80
- { exchange: "ex1", position: makePosition("BTC", "long", 0.02, 100000, 10, 100) },
81
- { exchange: "ex2", position: makePosition("ETH", "short", 1.0, 3500, 5, -50) },
82
- ];
83
- const result = assessRisk(balances, positions, defaultLimits);
84
- expect(result.metrics.totalEquity).toBe(8000);
85
- expect(result.metrics.totalUnrealizedPnl).toBe(50);
86
- expect(result.metrics.totalMarginUsed).toBe(3000);
87
- expect(result.metrics.positionCount).toBe(2);
88
- expect(result.metrics.totalExposure).toBe(2000 + 3500); // 0.02*100000 + 1.0*3500
89
- expect(result.metrics.largestPositionUsd).toBe(3500);
90
- expect(result.metrics.maxLeverageUsed).toBe(10);
91
- expect(result.metrics.marginUtilization).toBeCloseTo(37.5, 1);
92
- });
93
- it("should handle empty portfolio", () => {
94
- const result = assessRisk([], [], defaultLimits);
95
- expect(result.level).toBe("low");
96
- expect(result.canTrade).toBe(true);
97
- expect(result.metrics.totalEquity).toBe(0);
98
- });
99
- it("should handle multiple violations and pick highest severity", () => {
100
- const balances = [{ exchange: "test", balance: makeBalance(1000, 50, 950, -600) }]; // drawdown + margin
101
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.1, 100000, 50, -600) }]; // position + leverage
102
- const result = assessRisk(balances, positions, defaultLimits);
103
- expect(result.level).toBe("critical"); // drawdown is critical
104
- expect(result.violations.length).toBeGreaterThanOrEqual(3);
105
- expect(result.canTrade).toBe(false);
106
- });
107
- });
108
- describe("Pre-Trade Check", () => {
109
- it("should allow trade within limits", () => {
110
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, 0) }], defaultLimits);
111
- const result = preTradeCheck(assessment, 1000, 5);
112
- expect(result.allowed).toBe(true);
113
- });
114
- it("should block trade when trading is suspended (critical violation)", () => {
115
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, -600) }], [], defaultLimits);
116
- const result = preTradeCheck(assessment, 100, 2);
117
- expect(result.allowed).toBe(false);
118
- expect(result.reason).toContain("suspended");
119
- });
120
- it("should block trade exceeding max position size", () => {
121
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [], defaultLimits);
122
- const result = preTradeCheck(assessment, 6000, 5); // > $5000 limit
123
- expect(result.allowed).toBe(false);
124
- expect(result.reason).toContain("max position size");
125
- });
126
- it("should block trade exceeding total exposure", () => {
127
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(50000, 30000, 20000, 0) }], [{ exchange: "test", position: makePosition("BTC", "long", 0.19, 100000, 5, 0) }], // $19000
128
- defaultLimits);
129
- const result = preTradeCheck(assessment, 2000, 5); // 19000 + 2000 = 21000 > 20000
130
- expect(result.allowed).toBe(false);
131
- expect(result.reason).toContain("exposure");
132
- });
133
- it("should block trade exceeding max positions", () => {
134
- const limits = { ...defaultLimits, maxPositions: 1 };
135
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 5, 0) }], limits);
136
- const result = preTradeCheck(assessment, 500, 5); // would be 2nd position
137
- expect(result.allowed).toBe(false);
138
- expect(result.reason).toContain("max positions");
139
- });
140
- it("should block trade exceeding max leverage", () => {
141
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }], [], defaultLimits);
142
- const result = preTradeCheck(assessment, 500, 25); // 25x > 20x limit
143
- expect(result.allowed).toBe(false);
144
- expect(result.reason).toContain("Leverage");
145
- });
146
- });
147
- describe("calcLiquidationDistance", () => {
148
- it("should calculate distance for long position (liq below mark)", () => {
149
- // Long at $100000, liq at $80000 → 20% distance
150
- const dist = calcLiquidationDistance(100000, 80000, "long");
151
- expect(dist).toBeCloseTo(20, 1);
152
- });
153
- it("should calculate distance for short position (liq above mark)", () => {
154
- // Short at $100000, liq at $120000 → 20% distance
155
- const dist = calcLiquidationDistance(100000, 120000, "short");
156
- expect(dist).toBeCloseTo(20, 1);
157
- });
158
- it("should return Infinity when liquidation price is 0 or N/A", () => {
159
- expect(calcLiquidationDistance(100000, 0, "long")).toBe(Infinity);
160
- expect(calcLiquidationDistance(0, 80000, "long")).toBe(Infinity);
161
- });
162
- it("should handle very close liquidation (dangerous)", () => {
163
- // Long at $100000, liq at $95000 → 5% distance
164
- const dist = calcLiquidationDistance(100000, 95000, "long");
165
- expect(dist).toBeCloseTo(5, 1);
166
- });
167
- it("should handle very safe distance", () => {
168
- // Long at $100000, liq at $50000 → 50% distance
169
- const dist = calcLiquidationDistance(100000, 50000, "long");
170
- expect(dist).toBeCloseTo(50, 1);
171
- });
172
- });
173
- describe("getLiquidationDistances", () => {
174
- it("should return sorted by distance (closest first)", () => {
175
- const positions = [
176
- { exchange: "hl", position: makePosition("ETH", "long", 1, 3500, 5, 0, "3000") }, // ~14.3%
177
- { exchange: "pac", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "50000") }, // 50%
178
- { exchange: "hl", position: makePosition("SOL", "short", 10, 150, 10, 0, "165") }, // 10%
179
- ];
180
- const distances = getLiquidationDistances(positions, defaultLimits);
181
- expect(distances.length).toBe(3);
182
- expect(distances[0].symbol).toBe("SOL"); // closest to liq
183
- expect(distances[1].symbol).toBe("ETH");
184
- expect(distances[2].symbol).toBe("BTC"); // safest
185
- });
186
- it("should assign correct status based on limits", () => {
187
- const limits = { ...defaultLimits, minLiquidationDistance: 30 };
188
- const positions = [
189
- { exchange: "a", position: makePosition("A", "long", 1, 100, 2, 0, "85") }, // 15% → danger (< 30% user limit)
190
- { exchange: "b", position: makePosition("B", "long", 1, 100, 2, 0, "78") }, // 22% → danger (< 30% user limit)
191
- { exchange: "c", position: makePosition("C", "long", 1, 100, 2, 0, "65") }, // 35% → warning (< 30% * 1.5 = 45%)
192
- { exchange: "d", position: makePosition("D", "long", 1, 100, 2, 0, "40") }, // 60% → safe
193
- ];
194
- const distances = getLiquidationDistances(positions, limits);
195
- expect(distances.find(d => d.symbol === "A").status).toBe("danger");
196
- expect(distances.find(d => d.symbol === "B").status).toBe("danger");
197
- expect(distances.find(d => d.symbol === "C").status).toBe("warning");
198
- expect(distances.find(d => d.symbol === "D").status).toBe("safe");
199
- });
200
- it("should skip positions with liquidationPrice 0 or N/A", () => {
201
- const positions = [
202
- { exchange: "a", position: makePosition("BTC", "long", 1, 100000, 5, 0, "0") },
203
- { exchange: "b", position: makePosition("ETH", "long", 1, 3500, 5, 0, "N/A") },
204
- { exchange: "c", position: makePosition("SOL", "long", 1, 150, 5, 0, "120") }, // valid
205
- ];
206
- const distances = getLiquidationDistances(positions, defaultLimits);
207
- expect(distances.length).toBe(1);
208
- expect(distances[0].symbol).toBe("SOL");
209
- });
210
- });
211
- describe("Liquidation Distance in assessRisk", () => {
212
- it("should add violation when position is below user limit", () => {
213
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
214
- // BTC long at $100000, liq at $90000 → 10% distance (below 30% user limit)
215
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 10, 0, "90000") }];
216
- const result = assessRisk(balances, positions, defaultLimits);
217
- expect(result.violations.some(v => v.rule === "min_liquidation_distance")).toBe(true);
218
- });
219
- it("should not add violation when distance is above user limit", () => {
220
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
221
- // BTC long at $100000, liq at $50000 → 50% distance (safe)
222
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "50000") }];
223
- const result = assessRisk(balances, positions, defaultLimits);
224
- expect(result.violations.some(v => v.rule === "min_liquidation_distance")).toBe(false);
225
- });
226
- it("should report minLiquidationDistancePct in metrics", () => {
227
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
228
- const positions = [
229
- { exchange: "a", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "60000") }, // 40%
230
- { exchange: "b", position: makePosition("ETH", "short", 1, 3500, 5, 0, "4200") }, // 20%
231
- ];
232
- const result = assessRisk(balances, positions, defaultLimits);
233
- expect(result.metrics.minLiquidationDistancePct).toBeCloseTo(20, 1);
234
- });
235
- it("should report -1 for minLiquidationDistancePct when no positions", () => {
236
- const result = assessRisk([], [], defaultLimits);
237
- expect(result.metrics.minLiquidationDistancePct).toBe(-1);
238
- });
239
- it("should include liquidationDistances array in assessment", () => {
240
- const balances = [{ exchange: "test", balance: makeBalance(10000, 8000, 2000, 0) }];
241
- // BTC long at $100000, liq at $30000 → 70% distance (well above 30% * 1.5 = 45% → safe)
242
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.01, 100000, 2, 0, "30000") }];
243
- const result = assessRisk(balances, positions, defaultLimits);
244
- expect(result.liquidationDistances).toHaveLength(1);
245
- expect(result.liquidationDistances[0].symbol).toBe("BTC");
246
- expect(result.liquidationDistances[0].distancePct).toBeCloseTo(70, 1);
247
- expect(result.liquidationDistances[0].status).toBe("safe");
248
- });
249
- });
250
- describe("Percentage-based Risk Limits", () => {
251
- it("effectiveLimit should return min of USD and pct-of-equity", () => {
252
- expect(effectiveLimit(500, 10, 1000)).toBe(100); // 10% of $1000 = $100 < $500
253
- expect(effectiveLimit(500, 10, 10000)).toBe(500); // 10% of $10000 = $1000 > $500
254
- expect(effectiveLimit(500, undefined, 1000)).toBe(500); // no pct → use USD
255
- expect(effectiveLimit(500, 10, 0)).toBe(500); // zero equity → use USD
256
- });
257
- it("should use pct-based drawdown when equity is small", () => {
258
- const limits = { ...defaultLimits, maxDrawdownPct: 10 };
259
- // equity = $100, 10% = $10 effective drawdown limit. uPnL = -$15 > $10 → critical
260
- const balances = [{ exchange: "test", balance: makeBalance(100, 80, 20, -15) }];
261
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.0001, 100000, 2, -15) }];
262
- const result = assessRisk(balances, positions, limits);
263
- expect(result.violations.some(v => v.rule === "max_drawdown")).toBe(true);
264
- expect(result.level).toBe("critical");
265
- });
266
- it("should use pct-based position limit when equity is small", () => {
267
- const limits = { ...defaultLimits, maxPositionPct: 25 };
268
- // equity = $200, 25% = $50 effective position limit. Position = $100 > $50 → violation
269
- const balances = [{ exchange: "test", balance: makeBalance(200, 150, 50, 0) }];
270
- const positions = [{ exchange: "test", position: makePosition("BTC", "long", 0.001, 100000, 2, 0) }]; // $100
271
- const result = assessRisk(balances, positions, limits);
272
- expect(result.violations.some(v => v.rule === "max_position_size")).toBe(true);
273
- });
274
- it("preTradeCheck should use pct-based limits", () => {
275
- const limits = { ...defaultLimits, maxPositionPct: 25 };
276
- // equity = $200, 25% = $50 effective position limit
277
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(200, 150, 50, 0) }], [], limits);
278
- expect(preTradeCheck(assessment, 60, 2).allowed).toBe(false); // $60 > $50
279
- expect(preTradeCheck(assessment, 40, 2).allowed).toBe(true); // $40 < $50
280
- });
281
- it("should use stricter of USD and pct limits", () => {
282
- const limits = { ...defaultLimits, maxPositionUsd: 100, maxPositionPct: 25 };
283
- // equity = $1000, 25% = $250. USD limit = $100. Effective = $100 (USD is stricter)
284
- const assessment = assessRisk([{ exchange: "test", balance: makeBalance(1000, 800, 200, 0) }], [], limits);
285
- expect(preTradeCheck(assessment, 120, 2).allowed).toBe(false); // $120 > $100
286
- expect(preTradeCheck(assessment, 80, 2).allowed).toBe(true); // $80 < $100
287
- });
288
- });
@@ -1 +0,0 @@
1
- export {};