perp-cli 0.11.0 → 0.12.0

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