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,421 +0,0 @@
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
- }
@@ -1,3 +0,0 @@
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;