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.
- package/dist/index.js +84 -0
- package/dist/lib/chat-handler.js +39 -5
- package/dist/lib/crypto/index.js +11 -1
- package/dist/lib/crypto/market-scanner.js +569 -0
- package/dist/lib/crypto/news-fetcher.js +394 -0
- package/dist/lib/crypto/signal-generator.js +76 -10
- package/dist/lib/crypto/smc-analyzer.js +39 -7
- package/dist/lib/crypto/whale-tracker.js +508 -0
- package/dist/lib/slash-commands.js +18 -0
- package/dist/lib/ui.js +45 -1
- package/package.json +1 -1
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Crypto News Fetcher
|
|
4
|
+
* Fetches latest cryptocurrency news from free APIs
|
|
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.displayNews = displayNews;
|
|
11
|
+
exports.fetchCryptoNews = fetchCryptoNews;
|
|
12
|
+
exports.runCryptoNews = runCryptoNews;
|
|
13
|
+
/* global fetch */
|
|
14
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
15
|
+
const ora_1 = __importDefault(require("ora"));
|
|
16
|
+
// ============================================
|
|
17
|
+
// NEWS SOURCES
|
|
18
|
+
// ============================================
|
|
19
|
+
/**
|
|
20
|
+
* Fetch news from CryptoPanic (free public API)
|
|
21
|
+
*/
|
|
22
|
+
async function fetchCryptoPanicNews(filter) {
|
|
23
|
+
try {
|
|
24
|
+
// CryptoPanic free public feed
|
|
25
|
+
let url = "https://cryptopanic.com/api/free/v1/posts/?public=true";
|
|
26
|
+
if (filter) {
|
|
27
|
+
url += `¤cies=${filter.toUpperCase()}`;
|
|
28
|
+
}
|
|
29
|
+
const response = await fetch(url, {
|
|
30
|
+
headers: {
|
|
31
|
+
Accept: "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
const data = await response.json();
|
|
38
|
+
if (!data.results || !Array.isArray(data.results)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
return data.results.slice(0, 15).map((item) => ({
|
|
42
|
+
title: item.title || "No title",
|
|
43
|
+
description: item.title || "", // CryptoPanic doesn't always have description
|
|
44
|
+
url: item.url || "",
|
|
45
|
+
source: item.source?.title || "CryptoPanic",
|
|
46
|
+
publishedAt: item.published_at || new Date().toISOString(),
|
|
47
|
+
sentiment: item.votes ? determineSentiment(item.votes) : "neutral",
|
|
48
|
+
coins: item.currencies?.map((c) => c.code) || [],
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch news from CoinGecko status updates (free, no API key)
|
|
57
|
+
*/
|
|
58
|
+
async function fetchCoinGeckoNews() {
|
|
59
|
+
try {
|
|
60
|
+
const response = await fetch("https://api.coingecko.com/api/v3/news", {
|
|
61
|
+
headers: {
|
|
62
|
+
Accept: "application/json",
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
const data = await response.json();
|
|
69
|
+
if (!data.data || !Array.isArray(data.data)) {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
return data.data.slice(0, 10).map((item) => ({
|
|
73
|
+
title: item.title || "No title",
|
|
74
|
+
description: item.description || "",
|
|
75
|
+
url: item.url || "",
|
|
76
|
+
source: item.news_site || "CoinGecko",
|
|
77
|
+
publishedAt: item.updated_at || new Date().toISOString(),
|
|
78
|
+
sentiment: "neutral",
|
|
79
|
+
coins: [],
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Fetch from alternative free news API
|
|
88
|
+
*/
|
|
89
|
+
async function fetchAlternativeNews(query = "cryptocurrency") {
|
|
90
|
+
try {
|
|
91
|
+
// Using a free RSS-to-JSON service for crypto news
|
|
92
|
+
const feeds = [
|
|
93
|
+
`https://api.rss2json.com/v1/api.json?rss_url=https://cointelegraph.com/rss`,
|
|
94
|
+
`https://api.rss2json.com/v1/api.json?rss_url=https://coindesk.com/arc/outboundfeeds/rss/`,
|
|
95
|
+
];
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const feedUrl of feeds) {
|
|
98
|
+
try {
|
|
99
|
+
const response = await fetch(feedUrl);
|
|
100
|
+
if (response.ok) {
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
if (data.items && Array.isArray(data.items)) {
|
|
103
|
+
const newsItems = data.items.slice(0, 5).map((item) => ({
|
|
104
|
+
title: item.title || "No title",
|
|
105
|
+
description: stripHtml(item.description || ""),
|
|
106
|
+
url: item.link || "",
|
|
107
|
+
source: data.feed?.title || "Crypto News",
|
|
108
|
+
publishedAt: item.pubDate || new Date().toISOString(),
|
|
109
|
+
sentiment: "neutral",
|
|
110
|
+
coins: extractCoins(item.title + " " + item.description),
|
|
111
|
+
}));
|
|
112
|
+
results.push(...newsItems);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Skip failed feed
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// ============================================
|
|
127
|
+
// HELPER FUNCTIONS
|
|
128
|
+
// ============================================
|
|
129
|
+
function determineSentiment(votes) {
|
|
130
|
+
if (!votes)
|
|
131
|
+
return "neutral";
|
|
132
|
+
const positive = (votes.positive || 0) + (votes.liked || 0);
|
|
133
|
+
const negative = (votes.negative || 0) + (votes.disliked || 0);
|
|
134
|
+
if (positive > negative + 2)
|
|
135
|
+
return "positive";
|
|
136
|
+
if (negative > positive + 2)
|
|
137
|
+
return "negative";
|
|
138
|
+
return "neutral";
|
|
139
|
+
}
|
|
140
|
+
function stripHtml(html) {
|
|
141
|
+
return html
|
|
142
|
+
.replace(/<[^>]*>/g, "")
|
|
143
|
+
.replace(/ /g, " ")
|
|
144
|
+
.replace(/&/g, "&")
|
|
145
|
+
.replace(/</g, "<")
|
|
146
|
+
.replace(/>/g, ">")
|
|
147
|
+
.replace(/"/g, '"')
|
|
148
|
+
.replace(/'/g, "'")
|
|
149
|
+
.trim()
|
|
150
|
+
.slice(0, 200);
|
|
151
|
+
}
|
|
152
|
+
function extractCoins(text) {
|
|
153
|
+
const coins = [];
|
|
154
|
+
const coinPatterns = [
|
|
155
|
+
/\bBTC\b/gi,
|
|
156
|
+
/\bBitcoin\b/gi,
|
|
157
|
+
/\bETH\b/gi,
|
|
158
|
+
/\bEthereum\b/gi,
|
|
159
|
+
/\bSOL\b/gi,
|
|
160
|
+
/\bSolana\b/gi,
|
|
161
|
+
/\bXRP\b/gi,
|
|
162
|
+
/\bRipple\b/gi,
|
|
163
|
+
/\bADA\b/gi,
|
|
164
|
+
/\bCardano\b/gi,
|
|
165
|
+
/\bDOGE\b/gi,
|
|
166
|
+
/\bDogecoin\b/gi,
|
|
167
|
+
/\bBNB\b/gi,
|
|
168
|
+
/\bAVAX\b/gi,
|
|
169
|
+
/\bAvalanche\b/gi,
|
|
170
|
+
/\bDOT\b/gi,
|
|
171
|
+
/\bPolkadot\b/gi,
|
|
172
|
+
/\bLINK\b/gi,
|
|
173
|
+
/\bChainlink\b/gi,
|
|
174
|
+
/\bMATIC\b/gi,
|
|
175
|
+
/\bPolygon\b/gi,
|
|
176
|
+
/\bSHIB\b/gi,
|
|
177
|
+
/\bPEPE\b/gi,
|
|
178
|
+
];
|
|
179
|
+
const normalizeMap = {
|
|
180
|
+
bitcoin: "BTC",
|
|
181
|
+
btc: "BTC",
|
|
182
|
+
ethereum: "ETH",
|
|
183
|
+
eth: "ETH",
|
|
184
|
+
solana: "SOL",
|
|
185
|
+
sol: "SOL",
|
|
186
|
+
ripple: "XRP",
|
|
187
|
+
xrp: "XRP",
|
|
188
|
+
cardano: "ADA",
|
|
189
|
+
ada: "ADA",
|
|
190
|
+
dogecoin: "DOGE",
|
|
191
|
+
doge: "DOGE",
|
|
192
|
+
bnb: "BNB",
|
|
193
|
+
avalanche: "AVAX",
|
|
194
|
+
avax: "AVAX",
|
|
195
|
+
polkadot: "DOT",
|
|
196
|
+
dot: "DOT",
|
|
197
|
+
chainlink: "LINK",
|
|
198
|
+
link: "LINK",
|
|
199
|
+
polygon: "MATIC",
|
|
200
|
+
matic: "MATIC",
|
|
201
|
+
shib: "SHIB",
|
|
202
|
+
pepe: "PEPE",
|
|
203
|
+
};
|
|
204
|
+
for (const pattern of coinPatterns) {
|
|
205
|
+
const matches = text.match(pattern);
|
|
206
|
+
if (matches) {
|
|
207
|
+
for (const match of matches) {
|
|
208
|
+
const normalized = normalizeMap[match.toLowerCase()] || match.toUpperCase();
|
|
209
|
+
if (!coins.includes(normalized)) {
|
|
210
|
+
coins.push(normalized);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return coins;
|
|
216
|
+
}
|
|
217
|
+
function formatTimeAgo(dateStr) {
|
|
218
|
+
const date = new Date(dateStr);
|
|
219
|
+
const now = new Date();
|
|
220
|
+
const diffMs = now.getTime() - date.getTime();
|
|
221
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
222
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
223
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
224
|
+
if (diffMins < 1)
|
|
225
|
+
return "just now";
|
|
226
|
+
if (diffMins < 60)
|
|
227
|
+
return `${diffMins}m ago`;
|
|
228
|
+
if (diffHours < 24)
|
|
229
|
+
return `${diffHours}h ago`;
|
|
230
|
+
if (diffDays < 7)
|
|
231
|
+
return `${diffDays}d ago`;
|
|
232
|
+
return date.toLocaleDateString();
|
|
233
|
+
}
|
|
234
|
+
function getSentimentIcon(sentiment) {
|
|
235
|
+
switch (sentiment) {
|
|
236
|
+
case "positive":
|
|
237
|
+
return chalk_1.default.green("▲");
|
|
238
|
+
case "negative":
|
|
239
|
+
return chalk_1.default.red("▼");
|
|
240
|
+
default:
|
|
241
|
+
return chalk_1.default.gray("●");
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function getSentimentColor(sentiment) {
|
|
245
|
+
switch (sentiment) {
|
|
246
|
+
case "positive":
|
|
247
|
+
return chalk_1.default.green;
|
|
248
|
+
case "negative":
|
|
249
|
+
return chalk_1.default.red;
|
|
250
|
+
default:
|
|
251
|
+
return chalk_1.default.gray;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ============================================
|
|
255
|
+
// DISPLAY FUNCTION
|
|
256
|
+
// ============================================
|
|
257
|
+
function displayNews(result, filter) {
|
|
258
|
+
console.log("");
|
|
259
|
+
console.log(chalk_1.default.bold.cyan(" ╔═══════════════════════════════════════════════════════════════════╗"));
|
|
260
|
+
console.log(chalk_1.default.bold.cyan(" ║ 📰 LATEST CRYPTO NEWS & UPDATES ║"));
|
|
261
|
+
console.log(chalk_1.default.bold.cyan(" ╚═══════════════════════════════════════════════════════════════════╝"));
|
|
262
|
+
console.log("");
|
|
263
|
+
if (filter) {
|
|
264
|
+
console.log(chalk_1.default.bgYellow.black(` 🔍 Filtered: ${filter.toUpperCase()} `));
|
|
265
|
+
console.log("");
|
|
266
|
+
}
|
|
267
|
+
console.log(chalk_1.default.gray(` 📊 ${result.news.length} news items | ${new Date(result.timestamp).toLocaleString()}`));
|
|
268
|
+
console.log("");
|
|
269
|
+
if (result.news.length === 0) {
|
|
270
|
+
console.log(chalk_1.default.yellow(" ⚠️ No news found. Try again later or with a different filter."));
|
|
271
|
+
console.log("");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
for (let i = 0; i < result.news.length; i++) {
|
|
275
|
+
const news = result.news[i];
|
|
276
|
+
const timeAgo = formatTimeAgo(news.publishedAt);
|
|
277
|
+
const sentimentIcon = getSentimentIcon(news.sentiment || "neutral");
|
|
278
|
+
const sentimentColor = getSentimentColor(news.sentiment || "neutral");
|
|
279
|
+
// News box header
|
|
280
|
+
console.log(chalk_1.default.cyan(` ┌${"─".repeat(70)}┐`));
|
|
281
|
+
// News number and sentiment
|
|
282
|
+
const numBadge = chalk_1.default.bgCyan.black(` ${(i + 1).toString().padStart(2)} `);
|
|
283
|
+
console.log(` │ ${numBadge} ${sentimentIcon} ${chalk_1.default.bold.white(truncateText(news.title, 58))}`);
|
|
284
|
+
// Second line of title if needed
|
|
285
|
+
if (news.title.length > 58) {
|
|
286
|
+
console.log(` │ ${chalk_1.default.white(news.title.slice(55, 120))}`);
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk_1.default.cyan(` ├${"─".repeat(70)}┤`));
|
|
289
|
+
// Key highlights
|
|
290
|
+
console.log(` │ ${chalk_1.default.yellow("⏰")} ${chalk_1.default.dim("Time:")} ${chalk_1.default.white(timeAgo)} ${chalk_1.default.dim("•")} ${chalk_1.default.gray(news.source)}`);
|
|
291
|
+
// Related coins with badges
|
|
292
|
+
if (news.coins && news.coins.length > 0) {
|
|
293
|
+
const coinBadges = news.coins.map((c) => chalk_1.default.bgYellow.black(` ${c} `)).join(" ");
|
|
294
|
+
console.log(` │ ${chalk_1.default.yellow("🪙")} ${chalk_1.default.dim("Coins:")} ${coinBadges}`);
|
|
295
|
+
}
|
|
296
|
+
// Sentiment indicator
|
|
297
|
+
const sentimentText = news.sentiment === "positive"
|
|
298
|
+
? "Bullish"
|
|
299
|
+
: news.sentiment === "negative"
|
|
300
|
+
? "Bearish"
|
|
301
|
+
: "Neutral";
|
|
302
|
+
console.log(` │ ${chalk_1.default.yellow("📈")} ${chalk_1.default.dim("Sentiment:")} ${sentimentColor(sentimentText)}`);
|
|
303
|
+
// Description with bullet point
|
|
304
|
+
if (news.description && news.description.length > 20) {
|
|
305
|
+
console.log(` │`);
|
|
306
|
+
console.log(` │ ${chalk_1.default.cyan("►")} ${chalk_1.default.gray(truncateText(news.description, 65))}`);
|
|
307
|
+
}
|
|
308
|
+
// Full clickable URL
|
|
309
|
+
console.log(` │`);
|
|
310
|
+
console.log(` │ ${chalk_1.default.green("🔗")} ${chalk_1.default.blue.underline(news.url)}`);
|
|
311
|
+
console.log(chalk_1.default.cyan(` └${"─".repeat(70)}┘`));
|
|
312
|
+
console.log("");
|
|
313
|
+
}
|
|
314
|
+
// Sentiment summary box
|
|
315
|
+
const positive = result.news.filter((n) => n.sentiment === "positive").length;
|
|
316
|
+
const negative = result.news.filter((n) => n.sentiment === "negative").length;
|
|
317
|
+
const neutral = result.news.length - positive - negative;
|
|
318
|
+
console.log(chalk_1.default.gray(" ┌─────────────────────────────────────────────────────────────────────┐"));
|
|
319
|
+
console.log(chalk_1.default.gray(" │ 📊 SENTIMENT SUMMARY │"));
|
|
320
|
+
console.log(chalk_1.default.gray(" ├─────────────────────────────────────────────────────────────────────┤"));
|
|
321
|
+
console.log(chalk_1.default.gray(" │ ") +
|
|
322
|
+
chalk_1.default.green(`▲ ${positive} Bullish`) +
|
|
323
|
+
" " +
|
|
324
|
+
chalk_1.default.red(`▼ ${negative} Bearish`) +
|
|
325
|
+
" " +
|
|
326
|
+
chalk_1.default.gray(`● ${neutral} Neutral`) +
|
|
327
|
+
chalk_1.default.gray(" │"));
|
|
328
|
+
console.log(chalk_1.default.gray(" └─────────────────────────────────────────────────────────────────────┘"));
|
|
329
|
+
console.log("");
|
|
330
|
+
console.log(chalk_1.default.dim.italic(" 💡 Tip: Click on links to read the full article"));
|
|
331
|
+
console.log("");
|
|
332
|
+
}
|
|
333
|
+
function truncateText(text, maxLength) {
|
|
334
|
+
if (text.length <= maxLength)
|
|
335
|
+
return text;
|
|
336
|
+
return text.slice(0, maxLength - 3) + "...";
|
|
337
|
+
}
|
|
338
|
+
// ============================================
|
|
339
|
+
// MAIN FUNCTION
|
|
340
|
+
// ============================================
|
|
341
|
+
async function fetchCryptoNews(filter) {
|
|
342
|
+
const spinner = (0, ora_1.default)("Fetching latest crypto news...").start();
|
|
343
|
+
try {
|
|
344
|
+
// Fetch from multiple sources in parallel
|
|
345
|
+
const [cryptoPanicNews, alternativeNews] = await Promise.all([
|
|
346
|
+
fetchCryptoPanicNews(filter),
|
|
347
|
+
fetchAlternativeNews(),
|
|
348
|
+
]);
|
|
349
|
+
// Combine and deduplicate news
|
|
350
|
+
const allNews = [...cryptoPanicNews, ...alternativeNews];
|
|
351
|
+
// Remove duplicates based on similar titles
|
|
352
|
+
const uniqueNews = [];
|
|
353
|
+
const seenTitles = new Set();
|
|
354
|
+
for (const news of allNews) {
|
|
355
|
+
const normalizedTitle = news.title.toLowerCase().slice(0, 50);
|
|
356
|
+
if (!seenTitles.has(normalizedTitle)) {
|
|
357
|
+
seenTitles.add(normalizedTitle);
|
|
358
|
+
uniqueNews.push(news);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// Sort by date (newest first)
|
|
362
|
+
uniqueNews.sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime());
|
|
363
|
+
// Filter by coin if specified
|
|
364
|
+
let filteredNews = uniqueNews;
|
|
365
|
+
if (filter) {
|
|
366
|
+
const filterUpper = filter.toUpperCase();
|
|
367
|
+
filteredNews = uniqueNews.filter((news) => news.coins?.includes(filterUpper) ||
|
|
368
|
+
news.title.toUpperCase().includes(filterUpper) ||
|
|
369
|
+
news.description?.toUpperCase().includes(filterUpper));
|
|
370
|
+
}
|
|
371
|
+
spinner.succeed(`Fetched ${filteredNews.length} news items`);
|
|
372
|
+
const result = {
|
|
373
|
+
timestamp: Date.now(),
|
|
374
|
+
news: filteredNews.slice(0, 20),
|
|
375
|
+
totalCount: filteredNews.length,
|
|
376
|
+
};
|
|
377
|
+
return result;
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
spinner.fail("Failed to fetch news");
|
|
381
|
+
return {
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
news: [],
|
|
384
|
+
totalCount: 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Run news fetcher and display results
|
|
390
|
+
*/
|
|
391
|
+
async function runCryptoNews(filter) {
|
|
392
|
+
const result = await fetchCryptoNews(filter);
|
|
393
|
+
displayNews(result, filter);
|
|
394
|
+
}
|
|
@@ -20,13 +20,17 @@ const analyzer_1 = require("./analyzer");
|
|
|
20
20
|
const market_analyzer_1 = require("./market-analyzer");
|
|
21
21
|
const config_1 = require("../config");
|
|
22
22
|
const groq_provider_1 = require("../models/groq-provider");
|
|
23
|
+
const ui_1 = require("../ui");
|
|
23
24
|
/**
|
|
24
25
|
* Generate AI reasoning for the trading signal
|
|
25
26
|
*/
|
|
26
27
|
async function generateAIReasoning(symbol, signal, price, priceChange24h) {
|
|
27
28
|
const apiKey = (0, config_1.getApiKey)();
|
|
28
29
|
if (!apiKey) {
|
|
29
|
-
return
|
|
30
|
+
return {
|
|
31
|
+
text: "AI reasoning unavailable (no API key configured)",
|
|
32
|
+
tokens: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
33
|
+
};
|
|
30
34
|
}
|
|
31
35
|
const modelConfig = (0, config_1.getModelConfig)();
|
|
32
36
|
const provider = new groq_provider_1.GroqProvider(modelConfig.modelId, 0.7);
|
|
@@ -50,6 +54,7 @@ Key Observations:
|
|
|
50
54
|
${signal.reasoning.map((r) => `- ${r}`).join("\n")}
|
|
51
55
|
|
|
52
56
|
Provide a concise trading insight (2-3 sentences) explaining the signal and any cautions. Be direct and actionable.`;
|
|
57
|
+
const tokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
53
58
|
try {
|
|
54
59
|
const stream = provider.streamChat([{ role: "user", content: prompt }], [] // No tools needed
|
|
55
60
|
);
|
|
@@ -58,11 +63,35 @@ Provide a concise trading insight (2-3 sentences) explaining the signal and any
|
|
|
58
63
|
if (chunk.content && typeof chunk.content === "string") {
|
|
59
64
|
response += chunk.content;
|
|
60
65
|
}
|
|
66
|
+
// Capture token usage (check multiple formats)
|
|
67
|
+
if (chunk.usage_metadata) {
|
|
68
|
+
tokens.promptTokens =
|
|
69
|
+
chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
|
|
70
|
+
tokens.completionTokens =
|
|
71
|
+
chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
|
|
72
|
+
tokens.totalTokens =
|
|
73
|
+
chunk.usage_metadata.total_tokens || tokens.promptTokens + tokens.completionTokens;
|
|
74
|
+
}
|
|
75
|
+
if (chunk.response_metadata?.usage) {
|
|
76
|
+
const usage = chunk.response_metadata.usage;
|
|
77
|
+
tokens.promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
|
78
|
+
tokens.completionTokens = usage.completion_tokens || usage.output_tokens || 0;
|
|
79
|
+
tokens.totalTokens = usage.total_tokens || tokens.promptTokens + tokens.completionTokens;
|
|
80
|
+
}
|
|
61
81
|
}
|
|
62
|
-
|
|
82
|
+
// Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
|
|
83
|
+
if (tokens.totalTokens === 0 && response.length > 0) {
|
|
84
|
+
tokens.promptTokens = Math.ceil(prompt.length / 4);
|
|
85
|
+
tokens.completionTokens = Math.ceil(response.length / 4);
|
|
86
|
+
tokens.totalTokens = tokens.promptTokens + tokens.completionTokens;
|
|
87
|
+
}
|
|
88
|
+
return { text: response.trim(), tokens };
|
|
63
89
|
}
|
|
64
90
|
catch (error) {
|
|
65
|
-
return
|
|
91
|
+
return {
|
|
92
|
+
text: `AI reasoning unavailable: ${error.message}`,
|
|
93
|
+
tokens,
|
|
94
|
+
};
|
|
66
95
|
}
|
|
67
96
|
}
|
|
68
97
|
/**
|
|
@@ -78,10 +107,13 @@ async function generateTradingSignal(symbol, interval = "1h", includeAI = true)
|
|
|
78
107
|
const signal = (0, analyzer_1.generateSignal)(data);
|
|
79
108
|
spinner.text = "Generating trading signal...";
|
|
80
109
|
let aiReasoning;
|
|
110
|
+
let tokenUsage;
|
|
81
111
|
// Generate AI reasoning if requested
|
|
82
112
|
if (includeAI) {
|
|
83
113
|
spinner.text = "Getting AI insights...";
|
|
84
|
-
|
|
114
|
+
const aiResult = await generateAIReasoning(data.symbol, signal, data.currentPrice, data.priceChangePercent24h);
|
|
115
|
+
aiReasoning = aiResult.text;
|
|
116
|
+
tokenUsage = aiResult.tokens;
|
|
85
117
|
}
|
|
86
118
|
spinner.succeed(`Analysis complete for ${data.symbol}`);
|
|
87
119
|
return {
|
|
@@ -91,6 +123,7 @@ async function generateTradingSignal(symbol, interval = "1h", includeAI = true)
|
|
|
91
123
|
aiReasoning,
|
|
92
124
|
price: data.currentPrice,
|
|
93
125
|
priceChange24h: data.priceChangePercent24h,
|
|
126
|
+
tokenUsage,
|
|
94
127
|
};
|
|
95
128
|
}
|
|
96
129
|
catch (error) {
|
|
@@ -212,6 +245,10 @@ function displaySignal(result) {
|
|
|
212
245
|
}
|
|
213
246
|
// Footer
|
|
214
247
|
console.log(chalk_1.default.cyan("\u{2514}" + "\u{2500}".repeat(45) + "\u{2518}"));
|
|
248
|
+
// Token usage
|
|
249
|
+
if (result.tokenUsage && result.tokenUsage.totalTokens > 0) {
|
|
250
|
+
(0, ui_1.showTokenUsageCompact)(result.tokenUsage.promptTokens, result.tokenUsage.completionTokens, result.tokenUsage.totalTokens);
|
|
251
|
+
}
|
|
215
252
|
// Disclaimer
|
|
216
253
|
console.log("");
|
|
217
254
|
console.log(chalk_1.default.gray.italic(" \u{26A0}\u{FE0F} This is not financial advice. Always do your own research."));
|
|
@@ -240,7 +277,10 @@ async function fullSignal(symbol, interval = "1h") {
|
|
|
240
277
|
async function generateComprehensiveAIAnalysis(analysis) {
|
|
241
278
|
const apiKey = (0, config_1.getApiKey)();
|
|
242
279
|
if (!apiKey) {
|
|
243
|
-
return
|
|
280
|
+
return {
|
|
281
|
+
text: "AI analysis unavailable (no API key configured)",
|
|
282
|
+
tokens: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
283
|
+
};
|
|
244
284
|
}
|
|
245
285
|
const modelConfig = (0, config_1.getModelConfig)();
|
|
246
286
|
const provider = new groq_provider_1.GroqProvider(modelConfig.modelId, 0.7);
|
|
@@ -307,6 +347,7 @@ Provide a detailed trading analysis (4-6 sentences) that:
|
|
|
307
347
|
3. Mentions specific price levels to watch
|
|
308
348
|
4. Highlights any risks or cautions
|
|
309
349
|
Be specific with prices and actionable advice.`;
|
|
350
|
+
const tokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
310
351
|
try {
|
|
311
352
|
const stream = provider.streamChat([{ role: "user", content: prompt }], []);
|
|
312
353
|
let response = "";
|
|
@@ -314,11 +355,32 @@ Be specific with prices and actionable advice.`;
|
|
|
314
355
|
if (chunk.content && typeof chunk.content === "string") {
|
|
315
356
|
response += chunk.content;
|
|
316
357
|
}
|
|
358
|
+
// Capture token usage (check multiple formats)
|
|
359
|
+
if (chunk.usage_metadata) {
|
|
360
|
+
tokens.promptTokens =
|
|
361
|
+
chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
|
|
362
|
+
tokens.completionTokens =
|
|
363
|
+
chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
|
|
364
|
+
tokens.totalTokens =
|
|
365
|
+
chunk.usage_metadata.total_tokens || tokens.promptTokens + tokens.completionTokens;
|
|
366
|
+
}
|
|
367
|
+
if (chunk.response_metadata?.usage) {
|
|
368
|
+
const usage = chunk.response_metadata.usage;
|
|
369
|
+
tokens.promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
|
370
|
+
tokens.completionTokens = usage.completion_tokens || usage.output_tokens || 0;
|
|
371
|
+
tokens.totalTokens = usage.total_tokens || tokens.promptTokens + tokens.completionTokens;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
|
|
375
|
+
if (tokens.totalTokens === 0 && response.length > 0) {
|
|
376
|
+
tokens.promptTokens = Math.ceil(prompt.length / 4);
|
|
377
|
+
tokens.completionTokens = Math.ceil(response.length / 4);
|
|
378
|
+
tokens.totalTokens = tokens.promptTokens + tokens.completionTokens;
|
|
317
379
|
}
|
|
318
|
-
return response.trim();
|
|
380
|
+
return { text: response.trim(), tokens };
|
|
319
381
|
}
|
|
320
382
|
catch (error) {
|
|
321
|
-
return `AI analysis unavailable: ${error.message}
|
|
383
|
+
return { text: `AI analysis unavailable: ${error.message}`, tokens };
|
|
322
384
|
}
|
|
323
385
|
}
|
|
324
386
|
/**
|
|
@@ -357,7 +419,7 @@ function wordWrap(text, maxWidth) {
|
|
|
357
419
|
/**
|
|
358
420
|
* Display comprehensive analysis in terminal
|
|
359
421
|
*/
|
|
360
|
-
function displayComprehensiveAnalysis(analysis, aiAnalysis) {
|
|
422
|
+
function displayComprehensiveAnalysis(analysis, aiAnalysis, tokenUsage) {
|
|
361
423
|
const boxWidth = 55;
|
|
362
424
|
const contentWidth = boxWidth - 4;
|
|
363
425
|
const border = {
|
|
@@ -535,6 +597,10 @@ function displayComprehensiveAnalysis(analysis, aiAnalysis) {
|
|
|
535
597
|
}
|
|
536
598
|
// Footer
|
|
537
599
|
console.log(border.bot);
|
|
600
|
+
// Token usage
|
|
601
|
+
if (tokenUsage && tokenUsage.totalTokens > 0) {
|
|
602
|
+
(0, ui_1.showTokenUsageCompact)(tokenUsage.promptTokens, tokenUsage.completionTokens, tokenUsage.totalTokens);
|
|
603
|
+
}
|
|
538
604
|
console.log("");
|
|
539
605
|
console.log(chalk_1.default.gray.italic(" \u{26A0}\u{FE0F} This is not financial advice. Always do your own research."));
|
|
540
606
|
console.log("");
|
|
@@ -548,9 +614,9 @@ async function comprehensiveAnalysis(symbol) {
|
|
|
548
614
|
spinner.text = "Fetching 1H, 4H, and 1D data...";
|
|
549
615
|
const analysis = await (0, market_analyzer_1.analyzeMarket)(symbol);
|
|
550
616
|
spinner.text = "Generating AI insights...";
|
|
551
|
-
const
|
|
617
|
+
const aiResult = await generateComprehensiveAIAnalysis(analysis);
|
|
552
618
|
spinner.succeed(`Comprehensive analysis complete for ${analysis.symbol}`);
|
|
553
|
-
displayComprehensiveAnalysis(analysis,
|
|
619
|
+
displayComprehensiveAnalysis(analysis, aiResult.text, aiResult.tokens);
|
|
554
620
|
}
|
|
555
621
|
catch (error) {
|
|
556
622
|
spinner.fail(`Failed to analyze ${symbol}`);
|
|
@@ -15,6 +15,7 @@ const data_fetcher_1 = require("./data-fetcher");
|
|
|
15
15
|
const smc_indicators_1 = require("./smc-indicators");
|
|
16
16
|
const config_1 = require("../config");
|
|
17
17
|
const groq_provider_1 = require("../models/groq-provider");
|
|
18
|
+
const ui_1 = require("../ui");
|
|
18
19
|
const chart_visual_1 = require("./chart-visual");
|
|
19
20
|
// ============================================
|
|
20
21
|
// TRADE SETUP GENERATION
|
|
@@ -121,8 +122,12 @@ function generateSMCTradeSetup(smc, currentPrice) {
|
|
|
121
122
|
// ============================================
|
|
122
123
|
async function generateSMCAIAnalysis(analysis) {
|
|
123
124
|
const apiKey = (0, config_1.getApiKey)();
|
|
124
|
-
if (!apiKey)
|
|
125
|
-
return
|
|
125
|
+
if (!apiKey) {
|
|
126
|
+
return {
|
|
127
|
+
text: "AI analysis unavailable (no API key configured)",
|
|
128
|
+
tokens: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
126
131
|
const modelConfig = (0, config_1.getModelConfig)();
|
|
127
132
|
const provider = new groq_provider_1.GroqProvider(modelConfig.modelId, 0.7);
|
|
128
133
|
provider.initialize(apiKey, modelConfig.modelId);
|
|
@@ -173,6 +178,7 @@ Provide SMC-focused analysis explaining:
|
|
|
173
178
|
3. Best entry strategy (order block, FVG, or liquidity sweep)
|
|
174
179
|
4. Key levels to watch and when to enter
|
|
175
180
|
Be specific with prices and SMC terminology.`;
|
|
181
|
+
const tokens = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
176
182
|
try {
|
|
177
183
|
const stream = provider.streamChat([{ role: "user", content: prompt }], []);
|
|
178
184
|
let response = "";
|
|
@@ -180,11 +186,32 @@ Be specific with prices and SMC terminology.`;
|
|
|
180
186
|
if (chunk.content && typeof chunk.content === "string") {
|
|
181
187
|
response += chunk.content;
|
|
182
188
|
}
|
|
189
|
+
// Capture token usage (check multiple formats)
|
|
190
|
+
if (chunk.usage_metadata) {
|
|
191
|
+
tokens.promptTokens =
|
|
192
|
+
chunk.usage_metadata.input_tokens || chunk.usage_metadata.prompt_tokens || 0;
|
|
193
|
+
tokens.completionTokens =
|
|
194
|
+
chunk.usage_metadata.output_tokens || chunk.usage_metadata.completion_tokens || 0;
|
|
195
|
+
tokens.totalTokens =
|
|
196
|
+
chunk.usage_metadata.total_tokens || tokens.promptTokens + tokens.completionTokens;
|
|
197
|
+
}
|
|
198
|
+
if (chunk.response_metadata?.usage) {
|
|
199
|
+
const usage = chunk.response_metadata.usage;
|
|
200
|
+
tokens.promptTokens = usage.prompt_tokens || usage.input_tokens || 0;
|
|
201
|
+
tokens.completionTokens = usage.completion_tokens || usage.output_tokens || 0;
|
|
202
|
+
tokens.totalTokens = usage.total_tokens || tokens.promptTokens + tokens.completionTokens;
|
|
203
|
+
}
|
|
183
204
|
}
|
|
184
|
-
|
|
205
|
+
// Estimate tokens if not provided by API (rough estimate: ~4 chars per token)
|
|
206
|
+
if (tokens.totalTokens === 0 && response.length > 0) {
|
|
207
|
+
tokens.promptTokens = Math.ceil(prompt.length / 4);
|
|
208
|
+
tokens.completionTokens = Math.ceil(response.length / 4);
|
|
209
|
+
tokens.totalTokens = tokens.promptTokens + tokens.completionTokens;
|
|
210
|
+
}
|
|
211
|
+
return { text: response.trim(), tokens };
|
|
185
212
|
}
|
|
186
213
|
catch (error) {
|
|
187
|
-
return `AI analysis unavailable: ${error.message}
|
|
214
|
+
return { text: `AI analysis unavailable: ${error.message}`, tokens };
|
|
188
215
|
}
|
|
189
216
|
}
|
|
190
217
|
// ============================================
|
|
@@ -216,7 +243,7 @@ function wordWrap(text, maxWidth) {
|
|
|
216
243
|
lines.push(currentLine.trim());
|
|
217
244
|
return lines;
|
|
218
245
|
}
|
|
219
|
-
function displaySMCAnalysis(analysis, aiAnalysis) {
|
|
246
|
+
function displaySMCAnalysis(analysis, aiAnalysis, tokenUsage) {
|
|
220
247
|
const boxWidth = 58;
|
|
221
248
|
const contentWidth = boxWidth - 4;
|
|
222
249
|
const border = {
|
|
@@ -380,6 +407,11 @@ function displaySMCAnalysis(analysis, aiAnalysis) {
|
|
|
380
407
|
console.log("");
|
|
381
408
|
const smcChart = (0, chart_visual_1.createSMCVisualChart)(currentPrice, smc, tradeSetup);
|
|
382
409
|
smcChart.forEach((l) => console.log(l));
|
|
410
|
+
// Token usage
|
|
411
|
+
if (tokenUsage && tokenUsage.totalTokens > 0) {
|
|
412
|
+
console.log("");
|
|
413
|
+
(0, ui_1.showTokenUsageCompact)(tokenUsage.promptTokens, tokenUsage.completionTokens, tokenUsage.totalTokens);
|
|
414
|
+
}
|
|
383
415
|
console.log("");
|
|
384
416
|
console.log(chalk_1.default.gray.italic(" \u{26A0}\u{FE0F} This is not financial advice. Always do your own research."));
|
|
385
417
|
console.log("");
|
|
@@ -407,9 +439,9 @@ async function runSMCAnalysis(symbol) {
|
|
|
407
439
|
candles: data.candles,
|
|
408
440
|
};
|
|
409
441
|
spinner.text = "Getting AI insights...";
|
|
410
|
-
const
|
|
442
|
+
const aiResult = await generateSMCAIAnalysis(analysis);
|
|
411
443
|
spinner.succeed(`SMC analysis complete for ${data.symbol}`);
|
|
412
|
-
displaySMCAnalysis(analysis,
|
|
444
|
+
displaySMCAnalysis(analysis, aiResult.text, aiResult.tokens);
|
|
413
445
|
}
|
|
414
446
|
catch (error) {
|
|
415
447
|
spinner.fail(`Failed to analyze ${symbol}`);
|