prab-cli 1.2.4 → 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.
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ /**
3
+ * Market Scanner - Find Best Trading Opportunities
4
+ * Scans top cryptocurrencies and scores them based on multiple criteria
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.displayScanResults = displayScanResults;
11
+ exports.runMarketScanner = runMarketScanner;
12
+ /* global fetch */
13
+ const chalk_1 = __importDefault(require("chalk"));
14
+ const ora_1 = __importDefault(require("ora"));
15
+ const data_fetcher_1 = require("./data-fetcher");
16
+ const indicators_1 = require("./indicators");
17
+ // ============================================
18
+ // TOP CRYPTOCURRENCIES
19
+ // ============================================
20
+ // Top 100 by market cap (symbols for Binance)
21
+ const TOP_CRYPTOS = [
22
+ "BTC",
23
+ "ETH",
24
+ "BNB",
25
+ "SOL",
26
+ "XRP",
27
+ "ADA",
28
+ "AVAX",
29
+ "DOGE",
30
+ "DOT",
31
+ "LINK",
32
+ "MATIC",
33
+ "SHIB",
34
+ "LTC",
35
+ "ATOM",
36
+ "UNI",
37
+ "XLM",
38
+ "ETC",
39
+ "FIL",
40
+ "NEAR",
41
+ "APT",
42
+ "ARB",
43
+ "OP",
44
+ "INJ",
45
+ "IMX",
46
+ "VET",
47
+ "ALGO",
48
+ "FTM",
49
+ "SAND",
50
+ "MANA",
51
+ "AXS",
52
+ "GALA",
53
+ "THETA",
54
+ "EGLD",
55
+ "EOS",
56
+ "AAVE",
57
+ "MKR",
58
+ "GRT",
59
+ "SNX",
60
+ "CRV",
61
+ "LDO",
62
+ "RUNE",
63
+ "KAVA",
64
+ "ZEC",
65
+ "DASH",
66
+ "NEO",
67
+ "XTZ",
68
+ "IOTA",
69
+ "WAVES",
70
+ "CHZ",
71
+ "ENJ",
72
+ "BAT",
73
+ "1INCH",
74
+ "COMP",
75
+ "YFI",
76
+ "SUSHI",
77
+ "ZRX",
78
+ "ANKR",
79
+ "STORJ",
80
+ "SKL",
81
+ "CELO",
82
+ "ICX",
83
+ "ONT",
84
+ "QTUM",
85
+ "ZIL",
86
+ "IOST",
87
+ "SXP",
88
+ "RSR",
89
+ "REN",
90
+ "BAND",
91
+ "ALPHA",
92
+ "DENT",
93
+ "CELR",
94
+ "OGN",
95
+ "NKN",
96
+ "ARPA",
97
+ "CTSI",
98
+ "SLP",
99
+ "TLM",
100
+ "REEF",
101
+ "DODO",
102
+ "LINA",
103
+ "SUPER",
104
+ "TVK",
105
+ "BADGER",
106
+ "AUCTION",
107
+ "MASK",
108
+ "FRONT",
109
+ "AKRO",
110
+ "SUI",
111
+ "SEI",
112
+ "UNFI",
113
+ "WING",
114
+ "SFP",
115
+ "TKO",
116
+ "BURGER",
117
+ "PEPE",
118
+ "WIF",
119
+ "BONK",
120
+ "FLOKI",
121
+ "WLD",
122
+ ];
123
+ /**
124
+ * Format symbol for display (BTCUSDT -> BTC/USD)
125
+ */
126
+ function formatSymbolDisplay(binanceSymbol) {
127
+ // Remove USDT suffix and add /USD for cleaner display
128
+ if (binanceSymbol.endsWith("USDT")) {
129
+ return binanceSymbol.slice(0, -4) + "/USD";
130
+ }
131
+ if (binanceSymbol.endsWith("BTC")) {
132
+ return binanceSymbol.slice(0, -3) + "/BTC";
133
+ }
134
+ if (binanceSymbol.endsWith("ETH")) {
135
+ return binanceSymbol.slice(0, -3) + "/ETH";
136
+ }
137
+ return binanceSymbol;
138
+ }
139
+ // ============================================
140
+ // WICK ANALYSIS
141
+ // ============================================
142
+ /**
143
+ * Calculate wick ratio for candles
144
+ * Lower ratio = cleaner candles = more reliable price action
145
+ */
146
+ function calculateWickRatio(candles) {
147
+ let totalRatio = 0;
148
+ let cleanCount = 0;
149
+ for (const candle of candles) {
150
+ const body = Math.abs(candle.close - candle.open);
151
+ const totalRange = candle.high - candle.low;
152
+ if (totalRange === 0)
153
+ continue;
154
+ const wickRatio = 1 - body / totalRange; // 0 = all body, 1 = all wick
155
+ totalRatio += wickRatio;
156
+ // Clean candle = wick is less than 30% of total range
157
+ if (wickRatio < 0.3)
158
+ cleanCount++;
159
+ }
160
+ return {
161
+ avgRatio: totalRatio / candles.length,
162
+ cleanCandles: cleanCount,
163
+ };
164
+ }
165
+ /**
166
+ * Calculate upper and lower wick sizes
167
+ */
168
+ function analyzeWicks(candles) {
169
+ let totalUpperWick = 0;
170
+ let totalLowerWick = 0;
171
+ for (const candle of candles) {
172
+ const body = Math.abs(candle.close - candle.open);
173
+ const totalRange = candle.high - candle.low;
174
+ if (totalRange === 0)
175
+ continue;
176
+ const isGreen = candle.close >= candle.open;
177
+ const bodyTop = isGreen ? candle.close : candle.open;
178
+ const bodyBottom = isGreen ? candle.open : candle.close;
179
+ const upperWick = candle.high - bodyTop;
180
+ const lowerWick = bodyBottom - candle.low;
181
+ totalUpperWick += upperWick / totalRange;
182
+ totalLowerWick += lowerWick / totalRange;
183
+ }
184
+ const avgUpperWick = totalUpperWick / candles.length;
185
+ const avgLowerWick = totalLowerWick / candles.length;
186
+ // Positive = more upper wicks (bearish rejection), Negative = more lower wicks (bullish rejection)
187
+ const wickImbalance = avgUpperWick - avgLowerWick;
188
+ return { avgUpperWick, avgLowerWick, wickImbalance };
189
+ }
190
+ // ============================================
191
+ // SCORING FUNCTIONS
192
+ // ============================================
193
+ /**
194
+ * Calculate risk/reward score
195
+ */
196
+ function calculateRiskRewardScore(price, support, resistance) {
197
+ const distToSupport = ((price - support) / price) * 100;
198
+ const distToResist = ((resistance - price) / price) * 100;
199
+ // Risk = distance to support (stop loss area)
200
+ // Reward = distance to resistance (take profit area)
201
+ const ratio = distToResist / Math.max(distToSupport, 0.1);
202
+ // Score: higher ratio = better
203
+ let score = 0;
204
+ if (ratio >= 3)
205
+ score = 100;
206
+ else if (ratio >= 2.5)
207
+ score = 90;
208
+ else if (ratio >= 2)
209
+ score = 80;
210
+ else if (ratio >= 1.5)
211
+ score = 70;
212
+ else if (ratio >= 1)
213
+ score = 50;
214
+ else
215
+ score = 30;
216
+ return { score, ratio, distToSupport, distToResist };
217
+ }
218
+ /**
219
+ * Calculate signal strength score based on indicator confluence
220
+ */
221
+ function calculateSignalStrengthScore(rsi, macdCrossover, trend, priceVsEMA) {
222
+ let score = 50; // Base score
223
+ // RSI contribution
224
+ if (rsi < 30)
225
+ score += 20; // Oversold = bullish
226
+ else if (rsi < 40)
227
+ score += 10;
228
+ else if (rsi > 70)
229
+ score -= 20; // Overbought = bearish
230
+ else if (rsi > 60)
231
+ score -= 10;
232
+ // MACD contribution
233
+ if (macdCrossover === "bullish")
234
+ score += 15;
235
+ else if (macdCrossover === "bearish")
236
+ score -= 15;
237
+ // Trend contribution
238
+ if (trend === "bullish")
239
+ score += 15;
240
+ else if (trend === "bearish")
241
+ score -= 15;
242
+ // Price vs EMA contribution
243
+ if (priceVsEMA === "above_all")
244
+ score += 10;
245
+ else if (priceVsEMA === "below_all")
246
+ score -= 10;
247
+ return Math.max(0, Math.min(100, score));
248
+ }
249
+ /**
250
+ * Calculate momentum score
251
+ */
252
+ function calculateMomentumScore(change24h, volumeRatio, trendStrength) {
253
+ let score = 50;
254
+ // Price change contribution (positive change = bullish momentum)
255
+ if (change24h > 10)
256
+ score += 25;
257
+ else if (change24h > 5)
258
+ score += 15;
259
+ else if (change24h > 2)
260
+ score += 10;
261
+ else if (change24h < -10)
262
+ score -= 25;
263
+ else if (change24h < -5)
264
+ score -= 15;
265
+ else if (change24h < -2)
266
+ score -= 10;
267
+ // Volume contribution
268
+ if (volumeRatio > 2)
269
+ score += 20;
270
+ else if (volumeRatio > 1.5)
271
+ score += 10;
272
+ else if (volumeRatio < 0.5)
273
+ score -= 15;
274
+ // Trend strength
275
+ score += (trendStrength - 50) * 0.3;
276
+ return Math.max(0, Math.min(100, score));
277
+ }
278
+ /**
279
+ * Calculate wick score (higher = cleaner candles)
280
+ */
281
+ function calculateWickScore(avgWickRatio) {
282
+ // avgWickRatio: 0 = perfect (no wicks), 1 = all wicks
283
+ // We want to reward LOW wick ratios
284
+ const score = (1 - avgWickRatio) * 100;
285
+ return Math.max(0, Math.min(100, score));
286
+ }
287
+ // ============================================
288
+ // ANALYSIS FUNCTIONS
289
+ // ============================================
290
+ /**
291
+ * Analyze a single cryptocurrency
292
+ */
293
+ async function analyzeCrypto(symbol) {
294
+ try {
295
+ const data = await (0, data_fetcher_1.fetchCryptoData)(symbol, "4h", 100);
296
+ if (!data || data.candles.length < 50)
297
+ return null;
298
+ const closes = data.candles.map((c) => c.close);
299
+ const volumes = data.candles.map((c) => c.volume);
300
+ // Calculate indicators
301
+ const rsiResult = (0, indicators_1.calculateRSI)(closes, 14);
302
+ const currentRSI = rsiResult.current;
303
+ const macd = (0, indicators_1.calculateMACD)(closes);
304
+ const macdCrossover = macd.crossover;
305
+ const trend = (0, indicators_1.analyzeTrend)(data.candles);
306
+ const supportResistance = (0, indicators_1.calculateSupportResistance)(data.candles);
307
+ // Volume analysis
308
+ const recentVolume = volumes.slice(-5).reduce((a, b) => a + b, 0) / 5;
309
+ const avgVolume = volumes.slice(-20).reduce((a, b) => a + b, 0) / 20;
310
+ const volumeRatio = avgVolume > 0 ? recentVolume / avgVolume : 1;
311
+ // Wick analysis
312
+ const recentCandles = data.candles.slice(-20);
313
+ const wickData = calculateWickRatio(recentCandles);
314
+ const wickAnalysis = analyzeWicks(recentCandles);
315
+ // Calculate scores
316
+ const rrData = calculateRiskRewardScore(data.currentPrice, supportResistance.nearestSupport, supportResistance.nearestResistance);
317
+ // Determine price position relative to trend
318
+ const pricePosition = trend.direction === "bullish"
319
+ ? "above_all"
320
+ : trend.direction === "bearish"
321
+ ? "below_all"
322
+ : "neutral";
323
+ const signalStrengthScore = calculateSignalStrengthScore(currentRSI, macdCrossover, trend.direction, pricePosition);
324
+ const momentumScore = calculateMomentumScore(data.priceChangePercent24h, volumeRatio, trend.strength);
325
+ const wickScore = calculateWickScore(wickData.avgRatio);
326
+ // Calculate overall score (weighted average)
327
+ const overallScore = Math.round(rrData.score * 0.25 + signalStrengthScore * 0.3 + momentumScore * 0.25 + wickScore * 0.2);
328
+ // Determine signal
329
+ let signal = "HOLD";
330
+ if (overallScore >= 80 && signalStrengthScore >= 70)
331
+ signal = "STRONG_BUY";
332
+ else if (overallScore >= 65 && signalStrengthScore >= 55)
333
+ signal = "BUY";
334
+ else if (overallScore <= 30 && signalStrengthScore <= 35)
335
+ signal = "STRONG_SELL";
336
+ else if (overallScore <= 45 && signalStrengthScore <= 45)
337
+ signal = "SELL";
338
+ // Generate reasoning
339
+ const reasoning = [];
340
+ if (currentRSI < 35)
341
+ reasoning.push(`RSI oversold (${currentRSI.toFixed(0)})`);
342
+ if (currentRSI > 65)
343
+ reasoning.push(`RSI overbought (${currentRSI.toFixed(0)})`);
344
+ if (macdCrossover === "bullish")
345
+ reasoning.push("Bullish MACD crossover");
346
+ if (macdCrossover === "bearish")
347
+ reasoning.push("Bearish MACD crossover");
348
+ if (rrData.ratio >= 2)
349
+ reasoning.push(`Good R:R ratio (${rrData.ratio.toFixed(1)}:1)`);
350
+ if (wickScore >= 70)
351
+ reasoning.push("Clean price action (low wicks)");
352
+ if (wickScore < 40)
353
+ reasoning.push("Choppy price action (high wicks)");
354
+ if (volumeRatio > 1.5)
355
+ reasoning.push(`High volume (${volumeRatio.toFixed(1)}x avg)`);
356
+ return {
357
+ symbol: data.symbol,
358
+ price: data.currentPrice,
359
+ change24h: data.priceChangePercent24h,
360
+ overallScore,
361
+ riskRewardScore: rrData.score,
362
+ signalStrengthScore,
363
+ momentumScore,
364
+ wickScore,
365
+ rsi: currentRSI,
366
+ trend: trend.direction,
367
+ macdSignal: macdCrossover,
368
+ volumeRatio,
369
+ distanceToSupport: rrData.distToSupport,
370
+ distanceToResistance: rrData.distToResist,
371
+ riskRewardRatio: rrData.ratio,
372
+ avgWickRatio: wickData.avgRatio,
373
+ signal,
374
+ confidence: overallScore,
375
+ reasoning,
376
+ };
377
+ }
378
+ catch (error) {
379
+ return null;
380
+ }
381
+ }
382
+ // ============================================
383
+ // DISPLAY FUNCTIONS
384
+ // ============================================
385
+ function formatPrice(price) {
386
+ if (price < 0.001)
387
+ return `$${price.toFixed(6)}`;
388
+ if (price < 1)
389
+ return `$${price.toFixed(4)}`;
390
+ if (price < 100)
391
+ return `$${price.toFixed(2)}`;
392
+ return `$${price.toLocaleString(undefined, { maximumFractionDigits: 2 })}`;
393
+ }
394
+ function getScoreBar(score, width = 10) {
395
+ const filled = Math.round((score / 100) * width);
396
+ const empty = width - filled;
397
+ let color = chalk_1.default.green;
398
+ if (score < 40)
399
+ color = chalk_1.default.red;
400
+ else if (score < 60)
401
+ color = chalk_1.default.yellow;
402
+ return color("█".repeat(filled)) + chalk_1.default.gray("░".repeat(empty));
403
+ }
404
+ function getSignalColor(signal) {
405
+ switch (signal) {
406
+ case "STRONG_BUY":
407
+ return chalk_1.default.green.bold;
408
+ case "BUY":
409
+ return chalk_1.default.green;
410
+ case "SELL":
411
+ return chalk_1.default.red;
412
+ case "STRONG_SELL":
413
+ return chalk_1.default.red.bold;
414
+ default:
415
+ return chalk_1.default.yellow;
416
+ }
417
+ }
418
+ function displayScanResults(result) {
419
+ console.log("");
420
+ console.log(chalk_1.default.bold.cyan(" ═══════════════════════════════════════════════════════════════════"));
421
+ console.log(chalk_1.default.bold.cyan(" 🔍 MARKET SCANNER - TRADING OPPORTUNITIES "));
422
+ console.log(chalk_1.default.bold.cyan(" ═══════════════════════════════════════════════════════════════════"));
423
+ console.log("");
424
+ console.log(chalk_1.default.gray(` Scanned ${result.totalScanned} cryptocurrencies | ${new Date(result.timestamp).toLocaleString()}`));
425
+ console.log(chalk_1.default.gray(` Found ${result.opportunities.length} valid results`));
426
+ console.log("");
427
+ // Show all opportunities sorted by score
428
+ console.log(chalk_1.default.bold.yellow(" ┌─────────────────────────────────────────────────────────────────┐"));
429
+ console.log(chalk_1.default.bold.yellow(" │ 📊 ALL OPPORTUNITIES (Sorted) │"));
430
+ console.log(chalk_1.default.bold.yellow(" └─────────────────────────────────────────────────────────────────┘"));
431
+ console.log("");
432
+ console.log(chalk_1.default.gray(" Symbol Price 24h Score R:R Wick Signal"));
433
+ console.log(chalk_1.default.gray(" " + "─".repeat(65)));
434
+ // Show top 15 results
435
+ for (const opp of result.opportunities.slice(0, 15)) {
436
+ const changeColor = opp.change24h >= 0 ? chalk_1.default.green : chalk_1.default.red;
437
+ const changeStr = `${opp.change24h >= 0 ? "+" : ""}${opp.change24h.toFixed(1)}%`;
438
+ const displaySymbol = formatSymbolDisplay(opp.symbol);
439
+ console.log(chalk_1.default.white(` ${displaySymbol.padEnd(12)}`) +
440
+ chalk_1.default.yellow(`${formatPrice(opp.price).padEnd(14)}`) +
441
+ changeColor(`${changeStr.padEnd(8)}`) +
442
+ `${getScoreBar(opp.overallScore, 6)} ` +
443
+ chalk_1.default.cyan(`${opp.riskRewardRatio.toFixed(1)}:1`.padEnd(6)) +
444
+ `${getScoreBar(opp.wickScore, 4)} ` +
445
+ getSignalColor(opp.signal)(opp.signal));
446
+ if (opp.reasoning.length > 0) {
447
+ console.log(chalk_1.default.gray(` ${opp.reasoning.slice(0, 2).join(" | ")}`));
448
+ }
449
+ }
450
+ console.log("");
451
+ // Top Buy Opportunities
452
+ if (result.topBuys.length > 0) {
453
+ console.log(chalk_1.default.bold.green(" ┌─────────────────────────────────────────────────────────────────┐"));
454
+ console.log(chalk_1.default.bold.green(" │ 🟢 TOP BUY OPPORTUNITIES │"));
455
+ console.log(chalk_1.default.bold.green(" └─────────────────────────────────────────────────────────────────┘"));
456
+ console.log("");
457
+ console.log(chalk_1.default.gray(" Symbol Price 24h Score R:R Wick Signal"));
458
+ console.log(chalk_1.default.gray(" " + "─".repeat(65)));
459
+ }
460
+ for (const opp of result.topBuys.slice(0, 10)) {
461
+ const changeColor = opp.change24h >= 0 ? chalk_1.default.green : chalk_1.default.red;
462
+ const changeStr = `${opp.change24h >= 0 ? "+" : ""}${opp.change24h.toFixed(1)}%`;
463
+ const displaySymbol = formatSymbolDisplay(opp.symbol);
464
+ console.log(chalk_1.default.white(` ${displaySymbol.padEnd(12)}`) +
465
+ chalk_1.default.yellow(`${formatPrice(opp.price).padEnd(14)}`) +
466
+ changeColor(`${changeStr.padEnd(8)}`) +
467
+ `${getScoreBar(opp.overallScore, 6)} ` +
468
+ chalk_1.default.cyan(`${opp.riskRewardRatio.toFixed(1)}:1`.padEnd(6)) +
469
+ `${getScoreBar(opp.wickScore, 4)} ` +
470
+ getSignalColor(opp.signal)(opp.signal));
471
+ if (opp.reasoning.length > 0) {
472
+ console.log(chalk_1.default.gray(` ${opp.reasoning.slice(0, 2).join(" | ")}`));
473
+ }
474
+ }
475
+ console.log("");
476
+ // Top Sell Opportunities
477
+ if (result.topSells.length > 0) {
478
+ console.log(chalk_1.default.bold.red(" ┌─────────────────────────────────────────────────────────────────┐"));
479
+ console.log(chalk_1.default.bold.red(" │ 🔴 TOP SELL SIGNALS │"));
480
+ console.log(chalk_1.default.bold.red(" └─────────────────────────────────────────────────────────────────┘"));
481
+ console.log("");
482
+ for (const opp of result.topSells.slice(0, 5)) {
483
+ const changeColor = opp.change24h >= 0 ? chalk_1.default.green : chalk_1.default.red;
484
+ const changeStr = `${opp.change24h >= 0 ? "+" : ""}${opp.change24h.toFixed(1)}%`;
485
+ const displaySymbol = formatSymbolDisplay(opp.symbol);
486
+ console.log(chalk_1.default.white(` ${displaySymbol.padEnd(12)}`) +
487
+ chalk_1.default.yellow(`${formatPrice(opp.price).padEnd(14)}`) +
488
+ changeColor(`${changeStr.padEnd(8)}`) +
489
+ `${getScoreBar(opp.overallScore, 6)} ` +
490
+ getSignalColor(opp.signal)(opp.signal));
491
+ if (opp.reasoning.length > 0) {
492
+ console.log(chalk_1.default.gray(` ${opp.reasoning.slice(0, 2).join(" | ")}`));
493
+ }
494
+ }
495
+ console.log("");
496
+ }
497
+ // Cleanest Charts (Best for trading - low manipulation)
498
+ console.log(chalk_1.default.bold.cyan(" ┌─────────────────────────────────────────────────────────────────┐"));
499
+ console.log(chalk_1.default.bold.cyan(" │ 📊 CLEANEST CHARTS (Low Wick = Reliable) │"));
500
+ console.log(chalk_1.default.bold.cyan(" └─────────────────────────────────────────────────────────────────┘"));
501
+ console.log("");
502
+ console.log(chalk_1.default.gray(" Lower wick ratio = cleaner price action = more reliable trades"));
503
+ console.log("");
504
+ for (const opp of result.cleanestCharts.slice(0, 5)) {
505
+ const wickPct = ((1 - opp.avgWickRatio) * 100).toFixed(0);
506
+ const displaySymbol = formatSymbolDisplay(opp.symbol);
507
+ console.log(chalk_1.default.white(` ${displaySymbol.padEnd(12)}`) +
508
+ chalk_1.default.cyan(`Wick Score: ${opp.wickScore.toFixed(0)}/100`.padEnd(20)) +
509
+ chalk_1.default.green(`${wickPct}% body (clean)`.padEnd(18)) +
510
+ getSignalColor(opp.signal)(opp.signal));
511
+ }
512
+ console.log("");
513
+ // Legend
514
+ console.log(chalk_1.default.gray(" ─".repeat(33)));
515
+ console.log(chalk_1.default.gray(" Score: ") +
516
+ chalk_1.default.green("███") +
517
+ chalk_1.default.gray(" High (>60) ") +
518
+ chalk_1.default.yellow("███") +
519
+ chalk_1.default.gray(" Medium (40-60) ") +
520
+ chalk_1.default.red("███") +
521
+ chalk_1.default.gray(" Low (<40)"));
522
+ console.log(chalk_1.default.gray(" R:R = Risk/Reward Ratio | Wick = Price Action Cleanliness"));
523
+ console.log("");
524
+ console.log(chalk_1.default.gray.italic(" ⚠️ This is not financial advice. Always do your own research."));
525
+ console.log("");
526
+ }
527
+ // ============================================
528
+ // MAIN FUNCTION
529
+ // ============================================
530
+ async function runMarketScanner(limit = 50, minScore = 50) {
531
+ const spinner = (0, ora_1.default)("Scanning market for opportunities...").start();
532
+ const opportunities = [];
533
+ const cryptosToScan = TOP_CRYPTOS.slice(0, limit);
534
+ let scanned = 0;
535
+ const batchSize = 5; // Scan in batches to avoid rate limiting
536
+ for (let i = 0; i < cryptosToScan.length; i += batchSize) {
537
+ const batch = cryptosToScan.slice(i, i + batchSize);
538
+ spinner.text = `Scanning ${scanned + batch.length}/${cryptosToScan.length} cryptocurrencies...`;
539
+ const results = await Promise.all(batch.map((symbol) => analyzeCrypto(symbol)));
540
+ for (const result of results) {
541
+ if (result) {
542
+ opportunities.push(result);
543
+ }
544
+ }
545
+ scanned += batch.length;
546
+ // Small delay between batches to avoid rate limiting
547
+ if (i + batchSize < cryptosToScan.length) {
548
+ await new Promise((resolve) => setTimeout(resolve, 500));
549
+ }
550
+ }
551
+ spinner.succeed(`Scanned ${scanned} cryptocurrencies`);
552
+ // Sort and categorize
553
+ const sortedByScore = [...opportunities].sort((a, b) => b.overallScore - a.overallScore);
554
+ const topBuys = sortedByScore.filter((o) => o.signal === "STRONG_BUY" || o.signal === "BUY");
555
+ const topSells = sortedByScore.filter((o) => o.signal === "STRONG_SELL" || o.signal === "SELL");
556
+ const cleanestCharts = [...opportunities]
557
+ .sort((a, b) => b.wickScore - a.wickScore)
558
+ .filter((o) => o.wickScore >= 60);
559
+ const result = {
560
+ timestamp: Date.now(),
561
+ totalScanned: scanned,
562
+ opportunities: sortedByScore,
563
+ topBuys,
564
+ topSells,
565
+ cleanestCharts,
566
+ };
567
+ displayScanResults(result);
568
+ return result;
569
+ }