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,421 @@
1
+ import chalk from "chalk";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ import { resolve } from "path";
4
+ import { formatUsd, printJson, jsonOk } from "../utils.js";
5
+ import { computeAnnualSpread } from "../funding.js";
6
+ import { fetchPacificaPricesRaw, parsePacificaRaw, fetchHyperliquidAllMidsRaw, fetchHyperliquidMetaRaw, parseHyperliquidMetaRaw, } from "../shared-api.js";
7
+ const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
8
+ const ALERTS_FILE = resolve(PERP_DIR, "alerts.json");
9
+ function loadAlerts() {
10
+ if (!existsSync(PERP_DIR))
11
+ mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
12
+ if (!existsSync(ALERTS_FILE))
13
+ return { alerts: [], config: { telegramBotToken: "", telegramChatId: "", discordWebhook: "", slackWebhook: "" } };
14
+ try {
15
+ return JSON.parse(readFileSync(ALERTS_FILE, "utf-8"));
16
+ }
17
+ catch {
18
+ return { alerts: [], config: { telegramBotToken: "", telegramChatId: "", discordWebhook: "", slackWebhook: "" } };
19
+ }
20
+ }
21
+ function saveAlerts(store) {
22
+ if (!existsSync(PERP_DIR))
23
+ mkdirSync(PERP_DIR, { recursive: true, mode: 0o700 });
24
+ writeFileSync(ALERTS_FILE, JSON.stringify(store, null, 2), { mode: 0o600 });
25
+ }
26
+ function genId() {
27
+ return Math.random().toString(36).slice(2, 8);
28
+ }
29
+ // ── Notification Senders ──────────────────────────
30
+ async function sendTelegram(token, chatId, message) {
31
+ await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify({ chat_id: chatId, text: message, parse_mode: "Markdown" }),
35
+ });
36
+ }
37
+ async function sendDiscord(webhookUrl, message) {
38
+ await fetch(webhookUrl, {
39
+ method: "POST",
40
+ headers: { "Content-Type": "application/json" },
41
+ body: JSON.stringify({ content: message }),
42
+ });
43
+ }
44
+ async function sendSlack(webhookUrl, message) {
45
+ await fetch(webhookUrl, {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify({ text: message }),
49
+ });
50
+ }
51
+ async function notify(store, alert, message) {
52
+ const promises = [];
53
+ for (const ch of alert.channels) {
54
+ if (ch === "telegram" && store.config.telegramBotToken && store.config.telegramChatId) {
55
+ promises.push(sendTelegram(store.config.telegramBotToken, store.config.telegramChatId, message));
56
+ }
57
+ if (ch === "discord" && store.config.discordWebhook) {
58
+ promises.push(sendDiscord(store.config.discordWebhook, message));
59
+ }
60
+ if (ch === "slack" && store.config.slackWebhook) {
61
+ promises.push(sendSlack(store.config.slackWebhook, message));
62
+ }
63
+ }
64
+ await Promise.allSettled(promises);
65
+ }
66
+ // ── Alert Daemon ──────────────────────────────────
67
+ async function fetchPrices() {
68
+ const map = new Map();
69
+ try {
70
+ const [pacRes, hlRes] = await Promise.all([
71
+ fetchPacificaPricesRaw(),
72
+ fetchHyperliquidAllMidsRaw(),
73
+ ]);
74
+ const { prices: pacPrices } = parsePacificaRaw(pacRes);
75
+ for (const [sym, price] of pacPrices)
76
+ map.set(`pac:${sym}`, price);
77
+ if (hlRes && typeof hlRes === "object") {
78
+ for (const [symbol, price] of Object.entries(hlRes)) {
79
+ map.set(`hl:${symbol}`, Number(price));
80
+ }
81
+ }
82
+ // Merge into bare symbol keys: prefer pac price, fallback to hl
83
+ const allSymbols = new Set();
84
+ for (const k of map.keys()) {
85
+ if (k.includes(":"))
86
+ allSymbols.add(k.split(":")[1]);
87
+ }
88
+ for (const sym of allSymbols) {
89
+ const pacPrice = map.get(`pac:${sym}`);
90
+ const hlPrice = map.get(`hl:${sym}`);
91
+ map.set(sym, pacPrice ?? hlPrice ?? 0);
92
+ }
93
+ }
94
+ catch { }
95
+ return map;
96
+ }
97
+ async function fetchFundingRates() {
98
+ const map = new Map();
99
+ try {
100
+ const [pacRes, hlRes] = await Promise.all([
101
+ fetchPacificaPricesRaw(),
102
+ fetchHyperliquidMetaRaw(),
103
+ ]);
104
+ const { rates: pacRates } = parsePacificaRaw(pacRes);
105
+ const { rates: hlRates } = parseHyperliquidMetaRaw(hlRes);
106
+ const allSymbols = new Set([...pacRates.keys(), ...hlRates.keys()]);
107
+ for (const sym of allSymbols) {
108
+ const pac = pacRates.get(sym) ?? 0;
109
+ const hl = hlRates.get(sym) ?? 0;
110
+ const spread = computeAnnualSpread(pac, "pacifica", hl, "hyperliquid");
111
+ map.set(sym, { pac, hl, spread });
112
+ }
113
+ }
114
+ catch { }
115
+ return map;
116
+ }
117
+ async function fetchPositionData(getAdapterFor) {
118
+ const positions = [];
119
+ const accounts = [];
120
+ if (!getAdapterFor)
121
+ return { positions, accounts };
122
+ for (const exName of ["pacifica", "hyperliquid", "lighter"]) {
123
+ try {
124
+ const adapter = await getAdapterFor(exName);
125
+ const [pos, bal] = await Promise.all([
126
+ adapter.getPositions(),
127
+ adapter.getBalance(),
128
+ ]);
129
+ for (const p of pos) {
130
+ positions.push({ exchange: exName, symbol: p.symbol, pnl: parseFloat(p.unrealizedPnl || "0"), side: p.side, size: p.size });
131
+ }
132
+ accounts.push({
133
+ exchange: exName,
134
+ equity: Number(bal.equity ?? 0),
135
+ marginUsed: Number(bal.marginUsed ?? 0),
136
+ available: Number(bal.available ?? 0),
137
+ });
138
+ }
139
+ catch { /* exchange not configured or unreachable */ }
140
+ }
141
+ return { positions, accounts };
142
+ }
143
+ async function runDaemonCycle(store, getAdapterFor) {
144
+ const triggered = [];
145
+ const activeAlerts = store.alerts.filter(a => a.active);
146
+ if (activeAlerts.length === 0)
147
+ return triggered;
148
+ const prices = await fetchPrices();
149
+ const fundingRates = await fetchFundingRates();
150
+ // Only fetch position data if there are pnl/liquidation alerts
151
+ const hasPosAlerts = activeAlerts.some(a => a.type === "pnl" || a.type === "liquidation");
152
+ const posData = hasPosAlerts ? await fetchPositionData(getAdapterFor) : null;
153
+ for (const alert of activeAlerts) {
154
+ let fire = false;
155
+ let message = "";
156
+ if (alert.type === "price") {
157
+ const price = prices.get(alert.symbol.toUpperCase());
158
+ if (price === undefined)
159
+ continue;
160
+ if (alert.condition === "above" && price >= alert.value) {
161
+ fire = true;
162
+ message = `🔔 *${alert.symbol}* price above $${formatUsd(alert.value)}\nCurrent: $${formatUsd(price)}`;
163
+ }
164
+ else if (alert.condition === "below" && price <= alert.value) {
165
+ fire = true;
166
+ message = `🔔 *${alert.symbol}* price below $${formatUsd(alert.value)}\nCurrent: $${formatUsd(price)}`;
167
+ }
168
+ }
169
+ if (alert.type === "funding") {
170
+ const rate = fundingRates.get(alert.symbol.toUpperCase());
171
+ if (!rate)
172
+ continue;
173
+ if (rate.spread >= alert.value) {
174
+ fire = true;
175
+ message = `📊 *${alert.symbol}* funding spread: ${rate.spread.toFixed(1)}% annual\nPacifica: ${(rate.pac * 100).toFixed(4)}% | HL: ${(rate.hl * 100).toFixed(4)}%\nThreshold: ${alert.value}%`;
176
+ }
177
+ }
178
+ if (alert.type === "pnl" && posData) {
179
+ const sym = alert.symbol.toUpperCase();
180
+ const relevantPos = sym === "ALL"
181
+ ? posData.positions
182
+ : posData.positions.filter(p => p.symbol.toUpperCase() === sym);
183
+ const totalPnl = relevantPos.reduce((s, p) => s + p.pnl, 0);
184
+ if (alert.condition === "loss" && totalPnl <= -alert.value) {
185
+ fire = true;
186
+ const posInfo = relevantPos.map(p => `${p.exchange} ${p.side} ${p.size}: $${p.pnl.toFixed(2)}`).join("\n");
187
+ message = `🔴 *${sym}* uPnL: $${totalPnl.toFixed(2)} (threshold: -$${formatUsd(alert.value)})\n${posInfo}`;
188
+ }
189
+ else if (alert.condition === "profit" && totalPnl >= alert.value) {
190
+ fire = true;
191
+ const posInfo = relevantPos.map(p => `${p.exchange} ${p.side} ${p.size}: $${p.pnl.toFixed(2)}`).join("\n");
192
+ message = `🟢 *${sym}* uPnL: +$${totalPnl.toFixed(2)} (threshold: $${formatUsd(alert.value)})\n${posInfo}`;
193
+ }
194
+ }
195
+ if (alert.type === "liquidation" && posData) {
196
+ for (const acct of posData.accounts) {
197
+ if (acct.equity <= 0 || acct.marginUsed <= 0)
198
+ continue;
199
+ const marginRatio = ((acct.equity - acct.marginUsed) / acct.equity) * 100;
200
+ if (marginRatio <= alert.value) {
201
+ fire = true;
202
+ message = `⚠️ *${acct.exchange}* margin ratio: ${marginRatio.toFixed(1)}% (threshold: ${alert.value}%)\nEquity: $${formatUsd(acct.equity)} | Margin: $${formatUsd(acct.marginUsed)} | Available: $${formatUsd(acct.available)}`;
203
+ break; // one exchange triggering is enough
204
+ }
205
+ }
206
+ }
207
+ if (fire) {
208
+ triggered.push(alert.id);
209
+ await notify(store, alert, message);
210
+ }
211
+ }
212
+ return triggered;
213
+ }
214
+ // ── Commands ──────────────────────────────────────
215
+ export function registerAlertCommands(program, isJson, getAdapterFor) {
216
+ const alert = program.command("alert").description("Price & funding rate alerts (Telegram/Discord/Slack)");
217
+ // ── configure notification channels ──
218
+ alert
219
+ .command("config")
220
+ .description("Configure notification channels")
221
+ .option("--telegram-token <token>", "Telegram bot token")
222
+ .option("--telegram-chat <chatId>", "Telegram chat ID")
223
+ .option("--discord <webhookUrl>", "Discord webhook URL")
224
+ .option("--slack <webhookUrl>", "Slack webhook URL")
225
+ .action(async (opts) => {
226
+ const store = loadAlerts();
227
+ if (opts.telegramToken)
228
+ store.config.telegramBotToken = opts.telegramToken;
229
+ if (opts.telegramChat)
230
+ store.config.telegramChatId = opts.telegramChat;
231
+ if (opts.discord)
232
+ store.config.discordWebhook = opts.discord;
233
+ if (opts.slack)
234
+ store.config.slackWebhook = opts.slack;
235
+ saveAlerts(store);
236
+ if (isJson())
237
+ return printJson(jsonOk(store.config));
238
+ console.log(chalk.green("\n Notification config updated.\n"));
239
+ if (store.config.telegramBotToken)
240
+ console.log(` Telegram: ${chalk.green("configured")}`);
241
+ if (store.config.discordWebhook)
242
+ console.log(` Discord: ${chalk.green("configured")}`);
243
+ if (store.config.slackWebhook)
244
+ console.log(` Slack: ${chalk.green("configured")}`);
245
+ console.log();
246
+ });
247
+ // ── add alert ──
248
+ alert
249
+ .command("add")
250
+ .description("Add a new alert")
251
+ .requiredOption("-t, --type <type>", "Alert type: price, funding, pnl, liquidation")
252
+ .requiredOption("-s, --symbol <symbol>", "Symbol (e.g., BTC, ETH, SOL) or 'ALL' for portfolio-wide")
253
+ .option("--above <price>", "Price above threshold")
254
+ .option("--below <price>", "Price below threshold")
255
+ .option("--spread <pct>", "Funding spread threshold (annual %)")
256
+ .option("--loss <usd>", "PnL loss threshold in USD (triggers when uPnL drops below -N)")
257
+ .option("--profit <usd>", "PnL profit threshold in USD (triggers when uPnL exceeds N)")
258
+ .option("--margin-pct <pct>", "Liquidation proximity: alert when margin ratio below N%")
259
+ .option("--exchange <name>", "Exchange for pnl/liquidation alerts (default: all)")
260
+ .option("--telegram", "Send to Telegram")
261
+ .option("--discord", "Send to Discord")
262
+ .option("--slack", "Send to Slack")
263
+ .action(async (opts) => {
264
+ const channels = [];
265
+ if (opts.telegram)
266
+ channels.push("telegram");
267
+ if (opts.discord)
268
+ channels.push("discord");
269
+ if (opts.slack)
270
+ channels.push("slack");
271
+ if (channels.length === 0)
272
+ channels.push("telegram", "discord"); // default both
273
+ let condition = "";
274
+ let value = 0;
275
+ if (opts.type === "price") {
276
+ if (opts.above) {
277
+ condition = "above";
278
+ value = parseFloat(opts.above);
279
+ }
280
+ else if (opts.below) {
281
+ condition = "below";
282
+ value = parseFloat(opts.below);
283
+ }
284
+ else {
285
+ console.error(chalk.red(" Price alert needs --above or --below"));
286
+ process.exit(1);
287
+ }
288
+ }
289
+ else if (opts.type === "funding") {
290
+ value = parseFloat(opts.spread || "30");
291
+ condition = "spread";
292
+ }
293
+ else if (opts.type === "pnl") {
294
+ if (opts.loss) {
295
+ condition = "loss";
296
+ value = parseFloat(opts.loss);
297
+ }
298
+ else if (opts.profit) {
299
+ condition = "profit";
300
+ value = parseFloat(opts.profit);
301
+ }
302
+ else {
303
+ console.error(chalk.red(" PnL alert needs --loss or --profit"));
304
+ process.exit(1);
305
+ }
306
+ }
307
+ else if (opts.type === "liquidation") {
308
+ value = parseFloat(opts.marginPct || "20");
309
+ condition = "margin_low";
310
+ }
311
+ const store = loadAlerts();
312
+ const alert = {
313
+ id: genId(),
314
+ type: opts.type,
315
+ symbol: opts.symbol.toUpperCase(),
316
+ condition,
317
+ value,
318
+ channels,
319
+ active: true,
320
+ createdAt: new Date().toISOString(),
321
+ };
322
+ store.alerts.push(alert);
323
+ saveAlerts(store);
324
+ if (isJson())
325
+ return printJson(jsonOk(alert));
326
+ console.log(chalk.green(`\n Alert added: ${alert.id}`));
327
+ console.log(` Type: ${alert.type}`);
328
+ console.log(` Symbol: ${alert.symbol}`);
329
+ console.log(` Condition: ${condition} ${value}`);
330
+ console.log(` Channels: ${channels.join(", ")}\n`);
331
+ });
332
+ // ── list ──
333
+ alert
334
+ .command("list")
335
+ .description("List all alerts")
336
+ .action(async () => {
337
+ const store = loadAlerts();
338
+ if (isJson())
339
+ return printJson(jsonOk(store.alerts));
340
+ if (store.alerts.length === 0) {
341
+ console.log(chalk.gray("\n No alerts configured. Use 'perp alert add' to create one.\n"));
342
+ return;
343
+ }
344
+ console.log(chalk.cyan.bold("\n Alerts\n"));
345
+ for (const a of store.alerts) {
346
+ const status = a.active ? chalk.green("ON") : chalk.gray("OFF");
347
+ const desc = a.type === "price"
348
+ ? `${a.symbol} ${a.condition} $${formatUsd(a.value)}`
349
+ : a.type === "funding"
350
+ ? `${a.symbol} spread > ${a.value}%`
351
+ : a.type === "pnl"
352
+ ? `${a.symbol} ${a.condition === "loss" ? "loss > -" : "profit >"} $${formatUsd(a.value)}`
353
+ : `margin ratio < ${a.value}%`;
354
+ console.log(` ${status} ${chalk.white.bold(a.id)} ${desc} → ${a.channels.join(", ")}`);
355
+ }
356
+ console.log();
357
+ });
358
+ // ── remove ──
359
+ alert
360
+ .command("remove <id>")
361
+ .description("Remove an alert")
362
+ .action(async (id) => {
363
+ const store = loadAlerts();
364
+ store.alerts = store.alerts.filter(a => a.id !== id);
365
+ saveAlerts(store);
366
+ if (isJson())
367
+ return printJson(jsonOk({ removed: id }));
368
+ console.log(chalk.yellow(`\n Alert ${id} removed.\n`));
369
+ });
370
+ // ── test ──
371
+ alert
372
+ .command("test")
373
+ .description("Send a test notification to all configured channels")
374
+ .action(async () => {
375
+ const store = loadAlerts();
376
+ const testAlert = {
377
+ id: "test", type: "price", symbol: "TEST", condition: "test", value: 0,
378
+ channels: ["telegram", "discord", "slack"], active: true, createdAt: "",
379
+ };
380
+ if (!isJson())
381
+ console.log(chalk.cyan("\n Sending test notification...\n"));
382
+ await notify(store, testAlert, "🧪 *perp-cli* test alert — notifications working!");
383
+ if (isJson())
384
+ return printJson(jsonOk({ sent: true }));
385
+ console.log(chalk.green(" Sent! Check your Telegram/Discord/Slack.\n"));
386
+ });
387
+ // ── daemon ──
388
+ alert
389
+ .command("daemon")
390
+ .description("Run alert monitoring daemon (Ctrl+C to stop)")
391
+ .option("--interval <seconds>", "Check interval in seconds", "30")
392
+ .action(async (opts) => {
393
+ const intervalMs = parseInt(opts.interval) * 1000;
394
+ const store = loadAlerts();
395
+ const activeCount = store.alerts.filter(a => a.active).length;
396
+ if (!isJson()) {
397
+ console.log(chalk.cyan.bold("\n Alert Daemon Started\n"));
398
+ console.log(` Active alerts: ${activeCount}`);
399
+ console.log(` Check interval: ${opts.interval}s`);
400
+ console.log(` Telegram: ${store.config.telegramBotToken ? chalk.green("yes") : chalk.gray("no")}`);
401
+ console.log(` Discord: ${store.config.discordWebhook ? chalk.green("yes") : chalk.gray("no")}`);
402
+ console.log(` Slack: ${store.config.slackWebhook ? chalk.green("yes") : chalk.gray("no")}`);
403
+ console.log(chalk.gray("\n Monitoring... (Ctrl+C to stop)\n"));
404
+ }
405
+ const run = async () => {
406
+ try {
407
+ const fresh = loadAlerts(); // re-read in case alerts were added
408
+ const triggered = await runDaemonCycle(fresh, getAdapterFor);
409
+ if (triggered.length > 0) {
410
+ console.log(` ${chalk.yellow("⚡")} ${new Date().toLocaleTimeString()} — Triggered: ${triggered.join(", ")}`);
411
+ }
412
+ }
413
+ catch (err) {
414
+ console.error(chalk.gray(` Error: ${err instanceof Error ? err.message : String(err)}`));
415
+ }
416
+ };
417
+ await run();
418
+ setInterval(run, intervalMs);
419
+ await new Promise(() => { }); // keep alive
420
+ });
421
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from "commander";
2
+ import type { ExchangeAdapter } from "../exchanges/interface.js";
3
+ export declare function registerAnalyticsCommands(program: Command, getAdapterForExchange: (exchange: string) => Promise<ExchangeAdapter>, isJson: () => boolean): void;