prab-cli 1.2.1 → 1.2.5

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.
package/dist/index.js CHANGED
@@ -53,6 +53,8 @@ const file_tools_1 = require("./lib/tools/file-tools");
53
53
  const shell_tools_1 = require("./lib/tools/shell-tools");
54
54
  const git_tools_1 = require("./lib/tools/git-tools");
55
55
  const todo_tool_1 = require("./lib/tools/todo-tool");
56
+ // Import crypto signal system
57
+ const crypto_1 = require("./lib/crypto");
56
58
  // Import model system
57
59
  const groq_provider_1 = require("./lib/models/groq-provider");
58
60
  const registry_1 = require("./lib/models/registry");
@@ -98,6 +100,78 @@ program
98
100
  (0, config_1.clearApiKey)();
99
101
  ui_1.log.success("API Key cleared!");
100
102
  });
103
+ // SMC (Smart Money Concepts) analysis command
104
+ program
105
+ .command("smc <crypto>")
106
+ .description("Smart Money Concepts analysis (Order Blocks, FVG, Liquidity)")
107
+ .action(async (crypto) => {
108
+ await (0, crypto_1.runSMCAnalysis)(crypto);
109
+ });
110
+ // Comprehensive analysis command
111
+ program
112
+ .command("analyze <crypto>")
113
+ .description("Deep market analysis with multi-timeframe & all indicators")
114
+ .action(async (crypto) => {
115
+ await (0, crypto_1.comprehensiveAnalysis)(crypto);
116
+ });
117
+ // Quick trading signal command
118
+ program
119
+ .command("signal <crypto>")
120
+ .description("Quick trading signal for a cryptocurrency (e.g., prab-cli signal btc)")
121
+ .option("-i, --interval <interval>", "Time interval (1m, 5m, 15m, 1h, 4h, 1d, 1w)", "1h")
122
+ .action(async (crypto, options) => {
123
+ const validIntervals = ["1m", "5m", "15m", "1h", "4h", "1d", "1w"];
124
+ const interval = validIntervals.includes(options.interval)
125
+ ? options.interval
126
+ : "1h";
127
+ await (0, crypto_1.fullSignal)(crypto, interval);
128
+ });
129
+ // List supported cryptocurrencies
130
+ program
131
+ .command("crypto-list")
132
+ .description("List supported cryptocurrency symbols")
133
+ .action(() => {
134
+ console.log("\nSupported Cryptocurrencies:\n");
135
+ const symbols = (0, crypto_1.getSupportedSymbols)();
136
+ const columns = 4;
137
+ for (let i = 0; i < symbols.length; i += columns) {
138
+ const row = symbols
139
+ .slice(i, i + columns)
140
+ .map((s) => s.padEnd(12))
141
+ .join("");
142
+ console.log(" " + row);
143
+ }
144
+ console.log("\nYou can also use any Binance trading pair (e.g., BTCUSDT, ETHBTC)");
145
+ console.log("");
146
+ });
147
+ // Whale activity tracker
148
+ program
149
+ .command("whale")
150
+ .description("Track whale activity (large BTC/ETH transactions)")
151
+ .option("-c, --coins <coins>", "Coins to track (comma-separated)", "BTC,ETH")
152
+ .action(async (options) => {
153
+ const coins = options.coins.split(",").map((c) => c.trim().toUpperCase());
154
+ await (0, crypto_1.runWhaleTracker)(coins);
155
+ });
156
+ // Market scanner for opportunities
157
+ program
158
+ .command("scan")
159
+ .description("Scan market for best trading opportunities")
160
+ .option("-l, --limit <number>", "Number of cryptos to scan (max 100)", "50")
161
+ .option("-m, --min-score <number>", "Minimum score to display", "50")
162
+ .action(async (options) => {
163
+ const limit = Math.min(parseInt(options.limit) || 50, 100);
164
+ const minScore = parseInt(options.minScore) || 50;
165
+ await (0, crypto_1.runMarketScanner)(limit, minScore);
166
+ });
167
+ // Crypto news fetcher
168
+ program
169
+ .command("news")
170
+ .description("Get latest cryptocurrency news and updates")
171
+ .option("-c, --coin <coin>", "Filter news by specific coin (e.g., btc, eth)")
172
+ .action(async (options) => {
173
+ await (0, crypto_1.runCryptoNews)(options.coin);
174
+ });
101
175
  // Model management commands
102
176
  program
103
177
  .command("model")
@@ -239,6 +313,111 @@ program.action(async () => {
239
313
  }
240
314
  // Execute the selected command
241
315
  switch (action) {
316
+ case "smc": {
317
+ // Prompt for crypto symbol
318
+ const { cryptoSymbol } = await inquirer_1.default.prompt([
319
+ {
320
+ type: "input",
321
+ name: "cryptoSymbol",
322
+ message: "Enter cryptocurrency symbol (e.g., btc, eth, sol):",
323
+ default: "btc",
324
+ },
325
+ ]);
326
+ await (0, crypto_1.runSMCAnalysis)(cryptoSymbol);
327
+ break;
328
+ }
329
+ case "analyze": {
330
+ // Prompt for crypto symbol
331
+ const { cryptoSymbol } = await inquirer_1.default.prompt([
332
+ {
333
+ type: "input",
334
+ name: "cryptoSymbol",
335
+ message: "Enter cryptocurrency symbol (e.g., btc, eth, sol):",
336
+ default: "btc",
337
+ },
338
+ ]);
339
+ await (0, crypto_1.comprehensiveAnalysis)(cryptoSymbol);
340
+ break;
341
+ }
342
+ case "signal": {
343
+ // Prompt for crypto symbol
344
+ const { cryptoSymbol } = await inquirer_1.default.prompt([
345
+ {
346
+ type: "input",
347
+ name: "cryptoSymbol",
348
+ message: "Enter cryptocurrency symbol (e.g., btc, eth, sol):",
349
+ default: "btc",
350
+ },
351
+ ]);
352
+ // Prompt for interval
353
+ const intervalChoice = await (0, select_1.default)({
354
+ message: "Select time interval:",
355
+ choices: [
356
+ { name: "1 Hour (recommended)", value: "1h" },
357
+ { name: "15 Minutes", value: "15m" },
358
+ { name: "4 Hours", value: "4h" },
359
+ { name: "1 Day", value: "1d" },
360
+ ],
361
+ });
362
+ await (0, crypto_1.fullSignal)(cryptoSymbol, intervalChoice);
363
+ break;
364
+ }
365
+ case "whale": {
366
+ // Prompt for coins to track
367
+ const { coins } = await inquirer_1.default.prompt([
368
+ {
369
+ type: "input",
370
+ name: "coins",
371
+ message: "Enter coins to track (comma-separated):",
372
+ default: "BTC,ETH",
373
+ },
374
+ ]);
375
+ const coinList = coins.split(",").map((c) => c.trim().toUpperCase());
376
+ await (0, crypto_1.runWhaleTracker)(coinList);
377
+ break;
378
+ }
379
+ case "scan": {
380
+ // Prompt for scan options
381
+ const scanLimit = await (0, select_1.default)({
382
+ message: "How many cryptocurrencies to scan?",
383
+ choices: [
384
+ { name: "Top 20 (Quick)", value: 20 },
385
+ { name: "Top 50 (Recommended)", value: 50 },
386
+ { name: "Top 100 (Comprehensive)", value: 100 },
387
+ ],
388
+ });
389
+ await (0, crypto_1.runMarketScanner)(scanLimit, 50);
390
+ break;
391
+ }
392
+ case "news": {
393
+ // Prompt for optional coin filter
394
+ const newsFilterChoice = await (0, select_1.default)({
395
+ message: "Filter news by coin?",
396
+ choices: [
397
+ { name: "All Crypto News", value: "" },
398
+ { name: "Bitcoin (BTC)", value: "BTC" },
399
+ { name: "Ethereum (ETH)", value: "ETH" },
400
+ { name: "Solana (SOL)", value: "SOL" },
401
+ { name: "Other (specify)", value: "__other__" },
402
+ ],
403
+ });
404
+ let coinFilter;
405
+ if (newsFilterChoice === "__other__") {
406
+ const { customCoin } = await inquirer_1.default.prompt([
407
+ {
408
+ type: "input",
409
+ name: "customCoin",
410
+ message: "Enter coin symbol (e.g., XRP, ADA, DOGE):",
411
+ },
412
+ ]);
413
+ coinFilter = customCoin.trim().toUpperCase() || undefined;
414
+ }
415
+ else if (newsFilterChoice) {
416
+ coinFilter = newsFilterChoice;
417
+ }
418
+ await (0, crypto_1.runCryptoNews)(coinFilter);
419
+ break;
420
+ }
242
421
  case "model": {
243
422
  // Fetch models from Groq API if not cached
244
423
  if (cachedModels.length === 0) {
@@ -279,7 +458,7 @@ program.action(async () => {
279
458
  ui_1.log.info(`Already using ${selectedModel}`);
280
459
  }
281
460
  }
282
- catch (e) {
461
+ catch {
283
462
  // User cancelled with Ctrl+C
284
463
  }
285
464
  break;
@@ -526,7 +705,7 @@ program.action(async () => {
526
705
  ui_1.log.info("Keeping current model. You can try again or switch models with /model");
527
706
  }
528
707
  }
529
- catch (e) {
708
+ catch {
530
709
  // User cancelled with Ctrl+C
531
710
  ui_1.log.info("Model switch cancelled.");
532
711
  }
@@ -82,6 +82,10 @@ When you need to perform file operations, use the appropriate tools rather than
82
82
  let fullResponse = "";
83
83
  let toolCalls = [];
84
84
  const formatter = new ui_1.StreamFormatter();
85
+ // Track tokens for this specific request
86
+ let requestPromptTokens = 0;
87
+ let requestCompletionTokens = 0;
88
+ let requestTotalTokens = 0;
85
89
  process.stdout.write("\n");
86
90
  try {
87
91
  for await (const chunk of stream) {
@@ -98,11 +102,26 @@ When you need to perform file operations, use the appropriate tools rather than
98
102
  if (chunk.tool_calls && chunk.tool_calls.length > 0) {
99
103
  toolCalls = chunk.tool_calls;
100
104
  }
101
- // Capture usage metadata from the chunk
105
+ // Capture usage metadata from the chunk (LangChain uses different field names)
102
106
  if (chunk.usage_metadata) {
103
- this.usage.promptTokens += chunk.usage_metadata.input_tokens || 0;
104
- this.usage.completionTokens += chunk.usage_metadata.output_tokens || 0;
105
- this.usage.totalTokens += chunk.usage_metadata.total_tokens || 0;
107
+ requestPromptTokens =
108
+ chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
109
+ requestCompletionTokens =
110
+ chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
111
+ requestTotalTokens =
112
+ chunk.usage_metadata.total_tokens || requestPromptTokens + requestCompletionTokens;
113
+ // Also update cumulative stats
114
+ this.usage.promptTokens += requestPromptTokens;
115
+ this.usage.completionTokens += requestCompletionTokens;
116
+ this.usage.totalTokens += requestTotalTokens;
117
+ }
118
+ // Also check response_metadata (alternative LangChain format)
119
+ if (chunk.response_metadata?.usage) {
120
+ const usage = chunk.response_metadata.usage;
121
+ requestPromptTokens = usage.prompt_tokens || usage.input_tokens || 0;
122
+ requestCompletionTokens = usage.completion_tokens || usage.output_tokens || 0;
123
+ requestTotalTokens =
124
+ usage.total_tokens || requestPromptTokens + requestCompletionTokens;
106
125
  }
107
126
  }
108
127
  // Increment request count
@@ -120,7 +139,22 @@ When you need to perform file operations, use the appropriate tools rather than
120
139
  tracker_1.tracker.apiError(apiError.message, { stack: apiError.stack });
121
140
  throw apiError;
122
141
  }
123
- process.stdout.write("\n\n");
142
+ process.stdout.write("\n");
143
+ // Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
144
+ if (requestTotalTokens === 0 && fullResponse.length > 0) {
145
+ // Estimate based on message content length
146
+ const inputText = this.messages
147
+ .map((m) => (typeof m.content === "string" ? m.content : ""))
148
+ .join(" ");
149
+ requestPromptTokens = Math.ceil(inputText.length / 4);
150
+ requestCompletionTokens = Math.ceil(fullResponse.length / 4);
151
+ requestTotalTokens = requestPromptTokens + requestCompletionTokens;
152
+ }
153
+ // Show token usage for this request
154
+ if (requestTotalTokens > 0) {
155
+ (0, ui_1.showTokenUsageCompact)(requestPromptTokens, requestCompletionTokens, requestTotalTokens);
156
+ }
157
+ process.stdout.write("\n");
124
158
  // Log AI response if there's content
125
159
  if (fullResponse.length > 0) {
126
160
  tracker_1.tracker.aiResponse(fullResponse);
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ /**
3
+ * Technical Analysis Module
4
+ * Calculates EMA and generates trading signals
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.calculateEMA = calculateEMA;
8
+ exports.calculateAllEMAs = calculateAllEMAs;
9
+ exports.calculateIndicators = calculateIndicators;
10
+ exports.generateSignal = generateSignal;
11
+ exports.formatSignalSummary = formatSignalSummary;
12
+ /**
13
+ * Calculate Exponential Moving Average
14
+ */
15
+ function calculateEMA(prices, period) {
16
+ if (prices.length < period) {
17
+ return [];
18
+ }
19
+ const ema = [];
20
+ const multiplier = 2 / (period + 1);
21
+ // First EMA value is SMA of first 'period' prices
22
+ let sum = 0;
23
+ for (let i = 0; i < period; i++) {
24
+ sum += prices[i];
25
+ }
26
+ ema.push(sum / period);
27
+ // Calculate EMA for remaining prices
28
+ for (let i = period; i < prices.length; i++) {
29
+ const currentEMA = (prices[i] - ema[ema.length - 1]) * multiplier + ema[ema.length - 1];
30
+ ema.push(currentEMA);
31
+ }
32
+ return ema;
33
+ }
34
+ /**
35
+ * Calculate all EMA values
36
+ */
37
+ function calculateAllEMAs(candles) {
38
+ const closePrices = candles.map((c) => c.close);
39
+ return {
40
+ ema9: calculateEMA(closePrices, 9),
41
+ ema21: calculateEMA(closePrices, 21),
42
+ ema50: calculateEMA(closePrices, 50),
43
+ ema200: calculateEMA(closePrices, 200),
44
+ };
45
+ }
46
+ /**
47
+ * Detect EMA crossover patterns
48
+ */
49
+ function detectCrossover(shortEMA, longEMA, lookback = 3) {
50
+ if (shortEMA.length < lookback + 1 || longEMA.length < lookback + 1) {
51
+ return "none";
52
+ }
53
+ const currentShort = shortEMA[shortEMA.length - 1];
54
+ const currentLong = longEMA[longEMA.length - 1];
55
+ const prevShort = shortEMA[shortEMA.length - lookback];
56
+ const prevLong = longEMA[longEMA.length - lookback];
57
+ // Golden cross: short EMA crosses above long EMA
58
+ if (prevShort <= prevLong && currentShort > currentLong) {
59
+ return "golden";
60
+ }
61
+ // Death cross: short EMA crosses below long EMA
62
+ if (prevShort >= prevLong && currentShort < currentLong) {
63
+ return "death";
64
+ }
65
+ return "none";
66
+ }
67
+ /**
68
+ * Determine price position relative to EMAs
69
+ */
70
+ function getPriceVsEMA(currentPrice, ema9, ema21, ema50) {
71
+ const aboveCount = [ema9, ema21, ema50].filter((ema) => currentPrice > ema).length;
72
+ if (aboveCount === 3)
73
+ return "above_all";
74
+ if (aboveCount === 0)
75
+ return "below_all";
76
+ return "mixed";
77
+ }
78
+ /**
79
+ * Calculate trend strength based on EMA alignment
80
+ */
81
+ function calculateTrendStrength(currentPrice, ema9, ema21, ema50, ema200) {
82
+ let strength = 0;
83
+ // Check EMA alignment (bullish: 9 > 21 > 50 > 200)
84
+ if (ema9 > ema21)
85
+ strength += 20;
86
+ if (ema21 > ema50)
87
+ strength += 20;
88
+ if (ema200 !== undefined && ema50 > ema200)
89
+ strength += 20;
90
+ // Check price vs EMAs
91
+ if (currentPrice > ema9)
92
+ strength += 10;
93
+ if (currentPrice > ema21)
94
+ strength += 10;
95
+ if (currentPrice > ema50)
96
+ strength += 10;
97
+ if (ema200 !== undefined && currentPrice > ema200)
98
+ strength += 10;
99
+ return Math.min(strength, 100);
100
+ }
101
+ /**
102
+ * Calculate technical indicators
103
+ */
104
+ function calculateIndicators(data) {
105
+ const ema = calculateAllEMAs(data.candles);
106
+ const currentEMA9 = ema.ema9[ema.ema9.length - 1] || 0;
107
+ const currentEMA21 = ema.ema21[ema.ema21.length - 1] || 0;
108
+ const currentEMA50 = ema.ema50[ema.ema50.length - 1] || 0;
109
+ const currentEMA200 = ema.ema200.length > 0 ? ema.ema200[ema.ema200.length - 1] : undefined;
110
+ // Detect crossover between EMA9 and EMA21
111
+ const emaCrossover = detectCrossover(ema.ema9, ema.ema21);
112
+ // Price vs EMA position
113
+ const priceVsEMA = getPriceVsEMA(data.currentPrice, currentEMA9, currentEMA21, currentEMA50);
114
+ // Determine trend
115
+ let trend;
116
+ if (currentEMA9 > currentEMA21 && currentEMA21 > currentEMA50) {
117
+ trend = "bullish";
118
+ }
119
+ else if (currentEMA9 < currentEMA21 && currentEMA21 < currentEMA50) {
120
+ trend = "bearish";
121
+ }
122
+ else {
123
+ trend = "neutral";
124
+ }
125
+ // Calculate trend strength
126
+ const trendStrength = calculateTrendStrength(data.currentPrice, currentEMA9, currentEMA21, currentEMA50, currentEMA200);
127
+ return {
128
+ ema,
129
+ currentEMA9,
130
+ currentEMA21,
131
+ currentEMA50,
132
+ currentEMA200: currentEMA200 || 0,
133
+ emaCrossover,
134
+ priceVsEMA,
135
+ trend,
136
+ trendStrength,
137
+ };
138
+ }
139
+ /**
140
+ * Generate trading signal based on EMA analysis
141
+ */
142
+ function generateSignal(data) {
143
+ const indicators = calculateIndicators(data);
144
+ const reasoning = [];
145
+ let signal = "HOLD";
146
+ let confidence = 50;
147
+ let stopLoss = 3;
148
+ let takeProfit = 6;
149
+ // EMA Crossover signals
150
+ if (indicators.emaCrossover === "golden") {
151
+ signal = "BUY";
152
+ confidence += 25;
153
+ reasoning.push("Golden cross detected (EMA9 crossed above EMA21)");
154
+ }
155
+ else if (indicators.emaCrossover === "death") {
156
+ signal = "SELL";
157
+ confidence += 25;
158
+ reasoning.push("Death cross detected (EMA9 crossed below EMA21)");
159
+ }
160
+ // Price position relative to EMAs
161
+ if (indicators.priceVsEMA === "above_all") {
162
+ if (signal !== "SELL") {
163
+ signal = signal === "HOLD" ? "BUY" : signal;
164
+ confidence += 15;
165
+ }
166
+ reasoning.push("Price is above all major EMAs (9, 21, 50)");
167
+ }
168
+ else if (indicators.priceVsEMA === "below_all") {
169
+ if (signal !== "BUY") {
170
+ signal = signal === "HOLD" ? "SELL" : signal;
171
+ confidence += 15;
172
+ }
173
+ reasoning.push("Price is below all major EMAs (9, 21, 50)");
174
+ }
175
+ else {
176
+ reasoning.push("Price is mixed relative to EMAs - consolidation phase");
177
+ }
178
+ // Trend analysis
179
+ if (indicators.trend === "bullish") {
180
+ if (signal !== "SELL") {
181
+ confidence += 10;
182
+ }
183
+ reasoning.push("EMAs are aligned bullishly (EMA9 > EMA21 > EMA50)");
184
+ }
185
+ else if (indicators.trend === "bearish") {
186
+ if (signal !== "BUY") {
187
+ confidence += 10;
188
+ }
189
+ reasoning.push("EMAs are aligned bearishly (EMA9 < EMA21 < EMA50)");
190
+ }
191
+ else {
192
+ reasoning.push("EMAs show no clear trend alignment");
193
+ }
194
+ // EMA200 analysis (if available)
195
+ if (indicators.currentEMA200 > 0) {
196
+ if (data.currentPrice > indicators.currentEMA200) {
197
+ if (signal === "BUY")
198
+ confidence += 10;
199
+ reasoning.push("Price is above EMA200 (long-term bullish)");
200
+ }
201
+ else {
202
+ if (signal === "SELL")
203
+ confidence += 10;
204
+ reasoning.push("Price is below EMA200 (long-term bearish)");
205
+ }
206
+ }
207
+ // 24h price change consideration
208
+ if (Math.abs(data.priceChangePercent24h) > 5) {
209
+ if (data.priceChangePercent24h > 5 && signal === "BUY") {
210
+ reasoning.push(`Strong momentum: +${data.priceChangePercent24h.toFixed(2)}% in 24h`);
211
+ confidence += 5;
212
+ }
213
+ else if (data.priceChangePercent24h < -5 && signal === "SELL") {
214
+ reasoning.push(`Strong downward momentum: ${data.priceChangePercent24h.toFixed(2)}% in 24h`);
215
+ confidence += 5;
216
+ }
217
+ else if (Math.abs(data.priceChangePercent24h) > 10) {
218
+ reasoning.push(`Caution: High volatility (${data.priceChangePercent24h.toFixed(2)}% in 24h)`);
219
+ confidence -= 5;
220
+ }
221
+ }
222
+ // Adjust stop-loss and take-profit based on signal and confidence
223
+ if (signal === "BUY") {
224
+ // Set stop-loss below recent support (EMA21)
225
+ const distanceToEMA21 = ((data.currentPrice - indicators.currentEMA21) / data.currentPrice) * 100;
226
+ stopLoss = Math.max(2, Math.min(distanceToEMA21 + 1, 5));
227
+ takeProfit = stopLoss * 2; // 2:1 risk-reward ratio
228
+ }
229
+ else if (signal === "SELL") {
230
+ // For sell signals, stop-loss above resistance
231
+ const distanceToEMA21 = ((indicators.currentEMA21 - data.currentPrice) / data.currentPrice) * 100;
232
+ stopLoss = Math.max(2, Math.min(Math.abs(distanceToEMA21) + 1, 5));
233
+ takeProfit = stopLoss * 2;
234
+ }
235
+ // Cap confidence at 95%
236
+ confidence = Math.min(confidence, 95);
237
+ confidence = Math.max(confidence, 10);
238
+ // If no clear signals, suggest HOLD
239
+ if (reasoning.length <= 2 && indicators.emaCrossover === "none") {
240
+ signal = "HOLD";
241
+ reasoning.push("No clear trading signal - recommend waiting for better setup");
242
+ }
243
+ return {
244
+ signal,
245
+ confidence,
246
+ stopLoss: Math.round(stopLoss * 10) / 10,
247
+ takeProfit: Math.round(takeProfit * 10) / 10,
248
+ indicators,
249
+ reasoning,
250
+ };
251
+ }
252
+ /**
253
+ * Format signal for display
254
+ */
255
+ function formatSignalSummary(signal, symbol, price) {
256
+ const signalEmoji = signal.signal === "BUY" ? "\u{1F7E2}" : signal.signal === "SELL" ? "\u{1F534}" : "\u{1F7E1}";
257
+ const trendEmoji = signal.indicators.trend === "bullish"
258
+ ? "\u{2197}\u{FE0F}"
259
+ : signal.indicators.trend === "bearish"
260
+ ? "\u{2198}\u{FE0F}"
261
+ : "\u{27A1}\u{FE0F}";
262
+ return `
263
+ ${signalEmoji} Signal: ${signal.signal}
264
+ Confidence: ${signal.confidence}%
265
+ Stop-Loss: ${signal.stopLoss}%
266
+ Take-Profit: ${signal.takeProfit}%
267
+
268
+ Current Price: $${price.toLocaleString()}
269
+ Trend: ${signal.indicators.trend} ${trendEmoji}
270
+ EMA9: $${signal.indicators.currentEMA9.toFixed(2)}
271
+ EMA21: $${signal.indicators.currentEMA21.toFixed(2)}
272
+ EMA50: $${signal.indicators.currentEMA50.toFixed(2)}
273
+ ${signal.indicators.currentEMA200 > 0 ? `EMA200: $${signal.indicators.currentEMA200.toFixed(2)}` : ""}
274
+ `;
275
+ }