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 @@
1
+ export {};
@@ -0,0 +1,264 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { getLastSettlement, getMinutesSinceSettlement, aggressiveSettleBoost, estimateFundingUntilSettlement, computeBasisRisk, formatNotifyMessage, sendNotification, } from "../arb-utils.js";
3
+ // ── Settlement Timing Tests ──
4
+ describe("getMinutesSinceSettlement", () => {
5
+ it("returns correct minutes after HL settlement (hourly)", () => {
6
+ // 14:30 UTC — last HL settlement was at 14:00, so 30 minutes ago
7
+ const now = new Date("2024-06-15T14:30:00Z");
8
+ const mins = getMinutesSinceSettlement("hyperliquid", now);
9
+ expect(mins).toBeCloseTo(30, 0);
10
+ });
11
+ it("returns correct minutes after PAC settlement (every 1h, same as HL)", () => {
12
+ // 10:15 UTC — last PAC settlement was at 10:00, so 15 minutes ago
13
+ const now = new Date("2024-06-15T10:15:00Z");
14
+ const mins = getMinutesSinceSettlement("pacifica", now);
15
+ expect(mins).toBeCloseTo(15, 0);
16
+ });
17
+ it("returns small value right after settlement", () => {
18
+ // 16:02 UTC — 2 minutes after PAC settlement at 16:00
19
+ const now = new Date("2024-06-15T16:02:00Z");
20
+ const mins = getMinutesSinceSettlement("pacifica", now);
21
+ expect(mins).toBeCloseTo(2, 0);
22
+ });
23
+ it("returns correct value right before next settlement", () => {
24
+ // 07:55 UTC — 55 minutes since last PAC settlement at 07:00
25
+ const now = new Date("2024-06-15T07:55:00Z");
26
+ const mins = getMinutesSinceSettlement("pacifica", now);
27
+ expect(mins).toBeCloseTo(55, 0);
28
+ });
29
+ it("handles midnight correctly for PAC", () => {
30
+ // 00:05 UTC — 5 minutes after 00:00 PAC settlement
31
+ const now = new Date("2024-06-15T00:05:00Z");
32
+ const mins = getMinutesSinceSettlement("pacifica", now);
33
+ expect(mins).toBeCloseTo(5, 0);
34
+ });
35
+ it("handles Lighter same as Pacifica (both hourly)", () => {
36
+ const now = new Date("2024-06-15T10:00:00Z");
37
+ const pacMins = getMinutesSinceSettlement("pacifica", now);
38
+ const ltMins = getMinutesSinceSettlement("lighter", now);
39
+ expect(ltMins).toBe(pacMins);
40
+ });
41
+ });
42
+ describe("getLastSettlement", () => {
43
+ it("returns the exact settlement time for HL", () => {
44
+ const now = new Date("2024-06-15T14:30:00Z");
45
+ const last = getLastSettlement("hyperliquid", now);
46
+ expect(last.getUTCHours()).toBe(14);
47
+ expect(last.getUTCMinutes()).toBe(0);
48
+ });
49
+ it("returns same hour for PAC (hourly settlement, same as HL)", () => {
50
+ // At 00:05 the last PAC settlement is 00:00 same day
51
+ const now = new Date("2024-06-15T00:05:00Z");
52
+ const last = getLastSettlement("pacifica", now);
53
+ expect(last.getUTCHours()).toBe(0);
54
+ expect(last.getUTCDate()).toBe(15); // same day since 00:00 is a settlement
55
+ });
56
+ });
57
+ describe("aggressiveSettleBoost", () => {
58
+ it("returns > 1.0 immediately after both exchanges settle", () => {
59
+ // Right after both HL and PAC settle (e.g., 16:01)
60
+ const now = new Date("2024-06-15T16:01:00Z");
61
+ const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
62
+ expect(boost).toBeGreaterThan(1.0);
63
+ expect(boost).toBeLessThanOrEqual(1.5);
64
+ });
65
+ it("returns 1.5 at exactly settlement time", () => {
66
+ // At exactly 16:00:01 — both just settled
67
+ const now = new Date("2024-06-15T16:00:01Z");
68
+ const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
69
+ // HL settled 0.01min ago, PAC settled 0.01min ago
70
+ // min = ~0.01, factor = 1 + 0.5 * (1 - 0.01/10) = ~1.499
71
+ expect(boost).toBeGreaterThan(1.4);
72
+ expect(boost).toBeLessThanOrEqual(1.5);
73
+ });
74
+ it("returns 1.0 when far from settlement", () => {
75
+ // 14:30 — both HL and PAC settled at 14:00, 30 minutes ago
76
+ const now = new Date("2024-06-15T14:30:00Z");
77
+ const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
78
+ // HL settled 30min ago > 10 window, PAC settled 30min ago > 10 window
79
+ // min(30, 30) = 30 > 10, so boost = 1.0
80
+ expect(boost).toBe(1.0);
81
+ });
82
+ it("returns 1.0 for both exchanges when settled > window ago", () => {
83
+ // HL settled 15 minutes ago, PAC settled 15 minutes ago
84
+ // With window of 10, both are > 10 so boost = 1.0
85
+ const now = new Date("2024-06-15T08:15:00Z");
86
+ const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
87
+ // HL settled at 08:00 (15 min ago), PAC settled at 08:00 (15 min ago)
88
+ // min(15, 15) = 15 > 10 => 1.0
89
+ expect(boost).toBe(1.0);
90
+ });
91
+ it("decays linearly within window", () => {
92
+ // HL settled 5 minutes ago, PAC settled 5 minutes ago (at 08:05)
93
+ const now = new Date("2024-06-15T08:05:00Z");
94
+ const boost = aggressiveSettleBoost("hyperliquid", "pacifica", 10, now);
95
+ // min(5, 5) = 5, factor = 1 + 0.5 * (1 - 5/10) = 1.25
96
+ expect(boost).toBeCloseTo(1.25, 1);
97
+ });
98
+ });
99
+ // ── Funding Estimation Tests ──
100
+ describe("estimateFundingUntilSettlement", () => {
101
+ it("calculates correct cumulative HL funding", () => {
102
+ // 0.01% hourly rate, $1000 position, 4 hours until settlement
103
+ const result = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
104
+ // hlCumulative = 0.0001 * 1000 * 4 = 0.4
105
+ expect(result.hlCumulative).toBeCloseTo(0.4, 4);
106
+ });
107
+ it("calculates correct PAC payment (also hourly)", () => {
108
+ // PAC hourly rate = 0.00005, $1000 position, 4 hours
109
+ const result = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
110
+ // pacPayment = 0.00005 * 1000 * 4 = 0.2
111
+ expect(result.pacPayment).toBeCloseTo(0.2, 4);
112
+ });
113
+ it("calculates net funding correctly", () => {
114
+ const result = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
115
+ // net = 0.4 - 0.2 = 0.2
116
+ expect(result.netFunding).toBeCloseTo(0.2, 4);
117
+ });
118
+ it("handles zero rates", () => {
119
+ const result = estimateFundingUntilSettlement(0, 0, 1000, 8);
120
+ expect(result.hlCumulative).toBe(0);
121
+ expect(result.pacPayment).toBe(0);
122
+ expect(result.netFunding).toBe(0);
123
+ });
124
+ it("scales linearly with position size", () => {
125
+ const small = estimateFundingUntilSettlement(0.0001, 0.00005, 100, 4);
126
+ const big = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 4);
127
+ expect(big.hlCumulative).toBeCloseTo(small.hlCumulative * 10, 4);
128
+ expect(big.pacPayment).toBeCloseTo(small.pacPayment * 10, 4);
129
+ });
130
+ it("both sides scale with time (both hourly)", () => {
131
+ const short = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 2);
132
+ const long = estimateFundingUntilSettlement(0.0001, 0.00005, 1000, 8);
133
+ expect(long.hlCumulative).toBeCloseTo(short.hlCumulative * 4, 4);
134
+ expect(long.pacPayment).toBeCloseTo(short.pacPayment * 4, 4);
135
+ });
136
+ });
137
+ // ── Basis Risk Tests ──
138
+ describe("computeBasisRisk", () => {
139
+ it("detects divergence correctly", () => {
140
+ const result = computeBasisRisk(100, 104, 3);
141
+ // |100 - 104| / 102 * 100 = ~3.92%
142
+ expect(result.divergencePct).toBeCloseTo(3.92, 1);
143
+ expect(result.warning).toBe(true);
144
+ });
145
+ it("no warning when divergence is low", () => {
146
+ const result = computeBasisRisk(100, 101, 3);
147
+ // |100 - 101| / 100.5 * 100 = ~0.995%
148
+ expect(result.divergencePct).toBeCloseTo(1.0, 0);
149
+ expect(result.warning).toBe(false);
150
+ });
151
+ it("handles equal prices", () => {
152
+ const result = computeBasisRisk(50, 50, 3);
153
+ expect(result.divergencePct).toBe(0);
154
+ expect(result.warning).toBe(false);
155
+ });
156
+ it("handles zero prices", () => {
157
+ const result = computeBasisRisk(0, 100, 3);
158
+ expect(result.divergencePct).toBe(0);
159
+ expect(result.warning).toBe(false);
160
+ });
161
+ it("uses custom threshold", () => {
162
+ const result = computeBasisRisk(100, 101, 0.5);
163
+ // ~1% divergence > 0.5% threshold
164
+ expect(result.warning).toBe(true);
165
+ });
166
+ it("symmetric for long/short swap", () => {
167
+ const a = computeBasisRisk(100, 105, 3);
168
+ const b = computeBasisRisk(105, 100, 3);
169
+ expect(a.divergencePct).toBeCloseTo(b.divergencePct, 4);
170
+ expect(a.warning).toBe(b.warning);
171
+ });
172
+ });
173
+ // ── Notification Tests ──
174
+ describe("formatNotifyMessage", () => {
175
+ it("formats entry message", () => {
176
+ const msg = formatNotifyMessage("entry", {
177
+ symbol: "WIF", longExchange: "lighter", shortExchange: "pacifica",
178
+ size: 500, netSpread: 28.5,
179
+ });
180
+ expect(msg).toContain("WIF");
181
+ expect(msg).toContain("Long lighter");
182
+ expect(msg).toContain("Short pacifica");
183
+ expect(msg).toContain("$500");
184
+ expect(msg).toContain("28.5%");
185
+ });
186
+ it("formats exit message", () => {
187
+ const msg = formatNotifyMessage("exit", {
188
+ symbol: "ETH", pnl: 10.5, duration: "7d 3h",
189
+ });
190
+ expect(msg).toContain("ETH");
191
+ expect(msg).toContain("+$10.50");
192
+ expect(msg).toContain("7d 3h");
193
+ });
194
+ it("formats exit message with negative PnL", () => {
195
+ const msg = formatNotifyMessage("exit", {
196
+ symbol: "SOL", pnl: -5.25, duration: "2d",
197
+ });
198
+ expect(msg).toContain("SOL");
199
+ expect(msg).toContain("-$5.25");
200
+ });
201
+ it("formats reversal message", () => {
202
+ const msg = formatNotifyMessage("reversal", { symbol: "WIF" });
203
+ expect(msg).toContain("REVERSAL");
204
+ expect(msg).toContain("WIF");
205
+ });
206
+ it("formats margin message", () => {
207
+ const msg = formatNotifyMessage("margin", {
208
+ exchange: "Lighter", marginPct: 25.3, threshold: 30,
209
+ });
210
+ expect(msg).toContain("LOW MARGIN");
211
+ expect(msg).toContain("Lighter");
212
+ expect(msg).toContain("25.3%");
213
+ expect(msg).toContain("30.0%");
214
+ });
215
+ it("formats basis risk message", () => {
216
+ const msg = formatNotifyMessage("basis", {
217
+ symbol: "WIF", divergencePct: 4.2, longExchange: "LT", shortExchange: "PAC",
218
+ });
219
+ expect(msg).toContain("BASIS RISK");
220
+ expect(msg).toContain("WIF");
221
+ expect(msg).toContain("4.2%");
222
+ expect(msg).toContain("LT/PAC");
223
+ });
224
+ });
225
+ describe("sendNotification", () => {
226
+ it("sends Discord webhook with content field", async () => {
227
+ const mockFetch = vi.fn().mockResolvedValue({ ok: true });
228
+ await sendNotification("https://discord.com/api/webhooks/123/abc", "entry", { symbol: "BTC", longExchange: "HL", shortExchange: "PAC", size: 100, netSpread: 30 }, mockFetch);
229
+ expect(mockFetch).toHaveBeenCalledOnce();
230
+ const [url, opts] = mockFetch.mock.calls[0];
231
+ expect(url).toContain("discord.com/api/webhooks");
232
+ const body = JSON.parse(opts.body);
233
+ expect(body).toHaveProperty("content");
234
+ expect(body.content).toContain("BTC");
235
+ });
236
+ it("sends Telegram webhook with chat_id and text", async () => {
237
+ const mockFetch = vi.fn().mockResolvedValue({ ok: true });
238
+ await sendNotification("https://api.telegram.org/bot123:TOKEN/sendMessage?chat_id=456", "exit", { symbol: "ETH", pnl: 5, duration: "3h" }, mockFetch);
239
+ expect(mockFetch).toHaveBeenCalledOnce();
240
+ const [url, opts] = mockFetch.mock.calls[0];
241
+ expect(url).toContain("api.telegram.org");
242
+ const body = JSON.parse(opts.body);
243
+ expect(body).toHaveProperty("chat_id", "456");
244
+ expect(body).toHaveProperty("text");
245
+ expect(body.text).toContain("ETH");
246
+ });
247
+ it("sends generic webhook with JSON body", async () => {
248
+ const mockFetch = vi.fn().mockResolvedValue({ ok: true });
249
+ await sendNotification("https://my-api.example.com/webhook", "basis", { symbol: "SOL", divergencePct: 5 }, mockFetch);
250
+ expect(mockFetch).toHaveBeenCalledOnce();
251
+ const [url, opts] = mockFetch.mock.calls[0];
252
+ expect(url).toContain("my-api.example.com");
253
+ const body = JSON.parse(opts.body);
254
+ expect(body).toHaveProperty("event", "basis");
255
+ expect(body).toHaveProperty("message");
256
+ expect(body).toHaveProperty("data");
257
+ });
258
+ it("does not throw on fetch failure", async () => {
259
+ const mockFetch = vi.fn().mockRejectedValue(new Error("network error"));
260
+ // Should not throw
261
+ await sendNotification("https://discord.com/api/webhooks/123/abc", "entry", { symbol: "BTC" }, mockFetch);
262
+ expect(mockFetch).toHaveBeenCalledOnce();
263
+ });
264
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,341 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { calculateRSI, evaluateCondition, evaluateAllConditions, } from "../bot/conditions.js";
3
+ // ── Helper: build a snapshot with defaults ──
4
+ function makeSnapshot(overrides = {}) {
5
+ return {
6
+ price: 100,
7
+ high24h: 105,
8
+ low24h: 95,
9
+ volume24h: 1_000_000,
10
+ fundingRate: 0.0001,
11
+ volatility24h: 10,
12
+ rsi: 50,
13
+ spreadPct: 0.05,
14
+ ...overrides,
15
+ };
16
+ }
17
+ const defaultContext = {
18
+ equity: 10_000,
19
+ startTime: Date.now() - 60_000,
20
+ peakEquity: 10_500,
21
+ dailyPnl: -200,
22
+ };
23
+ // ═══════════════════════════════════════════════════
24
+ // RSI Calculation Tests
25
+ // ═══════════════════════════════════════════════════
26
+ describe("calculateRSI", () => {
27
+ it("returns NaN when insufficient data (fewer than period+1 prices)", () => {
28
+ // Need at least 15 prices for period=14 (to get 14 deltas)
29
+ const closes = [100, 101, 102, 103, 104]; // only 5 prices
30
+ expect(calculateRSI(closes, 14)).toBeNaN();
31
+ });
32
+ it("returns NaN for empty array", () => {
33
+ expect(calculateRSI([])).toBeNaN();
34
+ });
35
+ it("returns NaN for single price", () => {
36
+ expect(calculateRSI([100])).toBeNaN();
37
+ });
38
+ it("returns 100 when all changes are positive (14 consecutive ups)", () => {
39
+ // 15 prices: 100, 101, ..., 114 — all gains, no losses
40
+ const closes = Array.from({ length: 15 }, (_, i) => 100 + i);
41
+ const rsi = calculateRSI(closes, 14);
42
+ expect(rsi).toBe(100);
43
+ });
44
+ it("returns ~0 when all changes are negative (14 consecutive downs)", () => {
45
+ // 15 prices: 114, 113, ..., 100 — all losses, no gains
46
+ const closes = Array.from({ length: 15 }, (_, i) => 114 - i);
47
+ const rsi = calculateRSI(closes, 14);
48
+ expect(rsi).toBeCloseTo(0, 5);
49
+ });
50
+ it("returns 50 when all prices are the same (no movement)", () => {
51
+ const closes = Array.from({ length: 20 }, () => 100);
52
+ const rsi = calculateRSI(closes, 14);
53
+ // avgGain=0, avgLoss=0 => RSI=50 by convention
54
+ expect(rsi).toBe(50);
55
+ });
56
+ it("returns ~50 when gains and losses are perfectly balanced", () => {
57
+ // Alternating +1, -1 with enough data for Wilder smoothing to converge
58
+ const closes = [100];
59
+ for (let i = 1; i <= 100; i++) {
60
+ closes.push(closes[i - 1] + (i % 2 === 1 ? 1 : -1));
61
+ }
62
+ // With Wilder smoothing, alternating equal gains/losses converges toward 50
63
+ const rsi = calculateRSI(closes, 14);
64
+ expect(rsi).toBeGreaterThan(45);
65
+ expect(rsi).toBeLessThan(55);
66
+ });
67
+ it("computes correct RSI for a known sequence (textbook example)", () => {
68
+ // Classic textbook: 14-period RSI example
69
+ // Prices chosen so that first 14 deltas have known gains/losses
70
+ const closes = [
71
+ 44.34, 44.09, 44.15, 43.61, 44.33,
72
+ 44.83, 45.10, 45.42, 45.84, 46.08,
73
+ 45.89, 46.03, 45.61, 46.28, 46.28,
74
+ 46.00, 46.03, 46.41, 46.22, 46.21,
75
+ ];
76
+ // This is a well-known RSI example; 14 deltas from 20 prices
77
+ // First 14 deltas: -0.25, 0.06, -0.54, 0.72, 0.50, 0.27, 0.32, 0.42, 0.24, -0.19, 0.14, -0.42, 0.67, 0.00
78
+ // Gains: 0, 0.06, 0, 0.72, 0.50, 0.27, 0.32, 0.42, 0.24, 0, 0.14, 0, 0.67, 0 = 3.34, avg = 0.2386
79
+ // Losses: 0.25, 0, 0.54, 0, 0, 0, 0, 0, 0, 0.19, 0, 0.42, 0, 0 = 1.40, avg = 0.1000
80
+ // After Wilder smoothing for remaining 5 deltas...
81
+ // We just verify it's in a reasonable range (60-80 for this bullish data)
82
+ const rsi = calculateRSI(closes, 14);
83
+ expect(rsi).toBeGreaterThan(55);
84
+ expect(rsi).toBeLessThan(80);
85
+ });
86
+ it("works with period=7 (shorter period)", () => {
87
+ // 8 prices needed minimum for period=7
88
+ const closes = [100, 102, 101, 103, 105, 104, 106, 108];
89
+ const rsi = calculateRSI(closes, 7);
90
+ // Mostly up, should be above 50
91
+ expect(rsi).toBeGreaterThan(50);
92
+ expect(rsi).toBeLessThanOrEqual(100);
93
+ });
94
+ it("handles exactly period+1 prices (minimum required)", () => {
95
+ // Exactly 15 prices for period=14
96
+ const closes = Array.from({ length: 15 }, (_, i) => 100 + i * 0.5);
97
+ const rsi = calculateRSI(closes, 14);
98
+ // All ups => RSI = 100
99
+ expect(rsi).toBe(100);
100
+ });
101
+ it("handles large datasets correctly", () => {
102
+ // 200 prices with upward trend + noise
103
+ const closes = [1000];
104
+ for (let i = 1; i < 200; i++) {
105
+ // Trend up ~0.5 with noise +-2
106
+ closes.push(closes[i - 1] + 0.5 + (Math.sin(i) * 2));
107
+ }
108
+ const rsi = calculateRSI(closes, 14);
109
+ expect(rsi).toBeGreaterThan(0);
110
+ expect(rsi).toBeLessThan(100);
111
+ expect(Number.isFinite(rsi)).toBe(true);
112
+ });
113
+ it("RSI increases when more gains are added to the series", () => {
114
+ // Base: mixed data
115
+ const base = [100, 99, 101, 98, 102, 97, 103, 96, 104, 95, 105, 94, 106, 93, 107];
116
+ const rsiBase = calculateRSI(base, 14);
117
+ // Extend with strong gains
118
+ const bullish = [...base, 110, 115, 120, 125, 130];
119
+ const rsiBullish = calculateRSI(bullish, 14);
120
+ expect(rsiBullish).toBeGreaterThan(rsiBase);
121
+ });
122
+ it("RSI decreases when more losses are added to the series", () => {
123
+ const base = [100, 99, 101, 98, 102, 97, 103, 96, 104, 95, 105, 94, 106, 93, 107];
124
+ const rsiBase = calculateRSI(base, 14);
125
+ // Extend with strong losses
126
+ const bearish = [...base, 102, 97, 92, 87, 82];
127
+ const rsiBearish = calculateRSI(bearish, 14);
128
+ expect(rsiBearish).toBeLessThan(rsiBase);
129
+ });
130
+ it("returns value in [0, 100] range for random data", () => {
131
+ // Generate random-walk prices
132
+ const closes = [100];
133
+ for (let i = 1; i < 50; i++) {
134
+ closes.push(closes[i - 1] + (Math.random() - 0.5) * 4);
135
+ }
136
+ const rsi = calculateRSI(closes, 14);
137
+ expect(rsi).toBeGreaterThanOrEqual(0);
138
+ expect(rsi).toBeLessThanOrEqual(100);
139
+ });
140
+ it("default period is 14", () => {
141
+ const closes = Array.from({ length: 20 }, (_, i) => 100 + i);
142
+ const rsiDefault = calculateRSI(closes);
143
+ const rsi14 = calculateRSI(closes, 14);
144
+ expect(rsiDefault).toBe(rsi14);
145
+ });
146
+ });
147
+ // ═══════════════════════════════════════════════════
148
+ // RSI with real-world-like price data
149
+ // ═══════════════════════════════════════════════════
150
+ describe("calculateRSI with realistic price data", () => {
151
+ it("produces overbought signal (>70) in strong uptrend", () => {
152
+ // Simulate strong uptrend: ETH going from 2000 to 2400 over 30 candles
153
+ const closes = [];
154
+ for (let i = 0; i < 30; i++) {
155
+ // Strong consistent uptrend with small pullbacks
156
+ closes.push(2000 + i * 14 - (i % 3 === 0 ? 5 : 0));
157
+ }
158
+ const rsi = calculateRSI(closes, 14);
159
+ expect(rsi).toBeGreaterThan(70);
160
+ });
161
+ it("produces oversold signal (<30) in strong downtrend", () => {
162
+ // Simulate strong downtrend: ETH going from 2400 to 2000 over 30 candles
163
+ const closes = [];
164
+ for (let i = 0; i < 30; i++) {
165
+ closes.push(2400 - i * 14 + (i % 3 === 0 ? 5 : 0));
166
+ }
167
+ const rsi = calculateRSI(closes, 14);
168
+ expect(rsi).toBeLessThan(30);
169
+ });
170
+ it("hovers around 50 in choppy/sideways market", () => {
171
+ // Simulate choppy market: oscillating around 2000
172
+ const closes = [];
173
+ for (let i = 0; i < 40; i++) {
174
+ closes.push(2000 + Math.sin(i * 0.8) * 20);
175
+ }
176
+ const rsi = calculateRSI(closes, 14);
177
+ expect(rsi).toBeGreaterThan(30);
178
+ expect(rsi).toBeLessThan(70);
179
+ });
180
+ it("responds to trend reversal", () => {
181
+ // Downtrend then reversal
182
+ const closes = [];
183
+ // 20 candles of downtrend
184
+ for (let i = 0; i < 20; i++) {
185
+ closes.push(2400 - i * 10);
186
+ }
187
+ const rsiBeforeReversal = calculateRSI(closes, 14);
188
+ // 10 candles of uptrend (reversal)
189
+ for (let i = 0; i < 10; i++) {
190
+ closes.push(2200 + i * 15);
191
+ }
192
+ const rsiAfterReversal = calculateRSI(closes, 14);
193
+ expect(rsiAfterReversal).toBeGreaterThan(rsiBeforeReversal);
194
+ });
195
+ });
196
+ // ═══════════════════════════════════════════════════
197
+ // evaluateCondition tests for RSI
198
+ // ═══════════════════════════════════════════════════
199
+ describe("evaluateCondition — rsi_above", () => {
200
+ it("returns true when RSI exceeds the threshold", () => {
201
+ const snapshot = makeSnapshot({ rsi: 75 });
202
+ const cond = { type: "rsi_above", value: 70 };
203
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
204
+ });
205
+ it("returns false when RSI is below the threshold", () => {
206
+ const snapshot = makeSnapshot({ rsi: 65 });
207
+ const cond = { type: "rsi_above", value: 70 };
208
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
209
+ });
210
+ it("returns false when RSI equals the threshold (not strictly above)", () => {
211
+ const snapshot = makeSnapshot({ rsi: 70 });
212
+ const cond = { type: "rsi_above", value: 70 };
213
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
214
+ });
215
+ it("returns false when RSI is NaN (insufficient data)", () => {
216
+ const snapshot = makeSnapshot({ rsi: NaN });
217
+ const cond = { type: "rsi_above", value: 30 };
218
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
219
+ });
220
+ it("accepts string value and parses it", () => {
221
+ const snapshot = makeSnapshot({ rsi: 80 });
222
+ const cond = { type: "rsi_above", value: "70" };
223
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
224
+ });
225
+ });
226
+ describe("evaluateCondition — rsi_below", () => {
227
+ it("returns true when RSI is below the threshold", () => {
228
+ const snapshot = makeSnapshot({ rsi: 25 });
229
+ const cond = { type: "rsi_below", value: 30 };
230
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
231
+ });
232
+ it("returns false when RSI is above the threshold", () => {
233
+ const snapshot = makeSnapshot({ rsi: 45 });
234
+ const cond = { type: "rsi_below", value: 30 };
235
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
236
+ });
237
+ it("returns false when RSI equals the threshold", () => {
238
+ const snapshot = makeSnapshot({ rsi: 30 });
239
+ const cond = { type: "rsi_below", value: 30 };
240
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
241
+ });
242
+ it("returns false when RSI is NaN", () => {
243
+ const snapshot = makeSnapshot({ rsi: NaN });
244
+ const cond = { type: "rsi_below", value: 70 };
245
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
246
+ });
247
+ });
248
+ // ═══════════════════════════════════════════════════
249
+ // evaluateCondition tests for spread_above
250
+ // ═══════════════════════════════════════════════════
251
+ describe("evaluateCondition — spread_above", () => {
252
+ it("returns true when spread exceeds threshold", () => {
253
+ const snapshot = makeSnapshot({ spreadPct: 0.15 });
254
+ const cond = { type: "spread_above", value: 0.1 };
255
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(true);
256
+ });
257
+ it("returns false when spread is below threshold", () => {
258
+ const snapshot = makeSnapshot({ spreadPct: 0.03 });
259
+ const cond = { type: "spread_above", value: 0.1 };
260
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
261
+ });
262
+ it("returns false when spread equals threshold", () => {
263
+ const snapshot = makeSnapshot({ spreadPct: 0.1 });
264
+ const cond = { type: "spread_above", value: 0.1 };
265
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
266
+ });
267
+ it("returns false when spread is zero (tight book)", () => {
268
+ const snapshot = makeSnapshot({ spreadPct: 0 });
269
+ const cond = { type: "spread_above", value: 0.01 };
270
+ expect(evaluateCondition(cond, snapshot, defaultContext)).toBe(false);
271
+ });
272
+ });
273
+ // ═══════════════════════════════════════════════════
274
+ // evaluateAllConditions with RSI
275
+ // ═══════════════════════════════════════════════════
276
+ describe("evaluateAllConditions with RSI conditions", () => {
277
+ it("combines RSI with price condition in 'all' mode", () => {
278
+ const snapshot = makeSnapshot({ price: 110, rsi: 75 });
279
+ const conditions = [
280
+ { type: "price_above", value: 100 },
281
+ { type: "rsi_above", value: 70 },
282
+ ];
283
+ expect(evaluateAllConditions(conditions, snapshot, defaultContext, "all")).toBe(true);
284
+ });
285
+ it("fails 'all' mode when RSI condition not met", () => {
286
+ const snapshot = makeSnapshot({ price: 110, rsi: 65 });
287
+ const conditions = [
288
+ { type: "price_above", value: 100 },
289
+ { type: "rsi_above", value: 70 },
290
+ ];
291
+ expect(evaluateAllConditions(conditions, snapshot, defaultContext, "all")).toBe(false);
292
+ });
293
+ it("passes 'any' mode when only RSI condition is met", () => {
294
+ const snapshot = makeSnapshot({ price: 90, rsi: 25 });
295
+ const conditions = [
296
+ { type: "price_above", value: 100 },
297
+ { type: "rsi_below", value: 30 },
298
+ ];
299
+ expect(evaluateAllConditions(conditions, snapshot, defaultContext, "any")).toBe(true);
300
+ });
301
+ it("combines spread_above with rsi_below for entry signal", () => {
302
+ // Wide spread + oversold RSI = potential entry
303
+ const snapshot = makeSnapshot({ spreadPct: 0.5, rsi: 22 });
304
+ const conditions = [
305
+ { type: "spread_above", value: 0.3 },
306
+ { type: "rsi_below", value: 30 },
307
+ ];
308
+ expect(evaluateAllConditions(conditions, snapshot, defaultContext, "all")).toBe(true);
309
+ });
310
+ });
311
+ // ═══════════════════════════════════════════════════
312
+ // Existing conditions still work (regression)
313
+ // ═══════════════════════════════════════════════════
314
+ describe("evaluateCondition — regression for existing conditions", () => {
315
+ it("always returns true", () => {
316
+ const cond = { type: "always", value: 0 };
317
+ expect(evaluateCondition(cond, makeSnapshot(), defaultContext)).toBe(true);
318
+ });
319
+ it("price_above works", () => {
320
+ const snapshot = makeSnapshot({ price: 110 });
321
+ expect(evaluateCondition({ type: "price_above", value: 100 }, snapshot, defaultContext)).toBe(true);
322
+ expect(evaluateCondition({ type: "price_above", value: 120 }, snapshot, defaultContext)).toBe(false);
323
+ });
324
+ it("price_below works", () => {
325
+ const snapshot = makeSnapshot({ price: 90 });
326
+ expect(evaluateCondition({ type: "price_below", value: 100 }, snapshot, defaultContext)).toBe(true);
327
+ expect(evaluateCondition({ type: "price_below", value: 80 }, snapshot, defaultContext)).toBe(false);
328
+ });
329
+ it("volatility_above works", () => {
330
+ const snapshot = makeSnapshot({ volatility24h: 15 });
331
+ expect(evaluateCondition({ type: "volatility_above", value: 10 }, snapshot, defaultContext)).toBe(true);
332
+ });
333
+ it("funding_rate_above works", () => {
334
+ const snapshot = makeSnapshot({ fundingRate: 0.001 });
335
+ expect(evaluateCondition({ type: "funding_rate_above", value: 0.0005 }, snapshot, defaultContext)).toBe(true);
336
+ });
337
+ it("balance_above works", () => {
338
+ const ctx = { ...defaultContext, equity: 15000 };
339
+ expect(evaluateCondition({ type: "balance_above", value: 10000 }, makeSnapshot(), ctx)).toBe(true);
340
+ });
341
+ });
@@ -0,0 +1 @@
1
+ export {};