prab-cli 1.2.4 → 1.2.6
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 +218 -2
- package/dist/lib/chat-handler.js +39 -5
- package/dist/lib/crypto/data-fetcher.js +86 -0
- package/dist/lib/crypto/ict-strategy.js +955 -0
- package/dist/lib/crypto/index.js +23 -1
- package/dist/lib/crypto/market-scanner.js +569 -0
- package/dist/lib/crypto/news-fetcher.js +394 -0
- package/dist/lib/crypto/orderblock-strategy.js +445 -0
- package/dist/lib/crypto/signal-generator.js +144 -17
- package/dist/lib/crypto/smc-analyzer.js +39 -7
- package/dist/lib/crypto/strategy-engine.js +803 -0
- package/dist/lib/crypto/whale-tracker.js +508 -0
- package/dist/lib/slash-commands.js +36 -0
- package/dist/lib/ui.js +45 -1
- package/dist/server/index.js +501 -0
- package/package.json +7 -1
package/dist/index.js
CHANGED
|
@@ -144,6 +144,76 @@ program
|
|
|
144
144
|
console.log("\nYou can also use any Binance trading pair (e.g., BTCUSDT, ETHBTC)");
|
|
145
145
|
console.log("");
|
|
146
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
|
+
});
|
|
175
|
+
// Smart trading strategy
|
|
176
|
+
program
|
|
177
|
+
.command("strategy <crypto>")
|
|
178
|
+
.description("Generate smart trading strategy with entry, exit, and leverage")
|
|
179
|
+
.option("-s, --style <style>", "Trading style: conservative, moderate, aggressive", "moderate")
|
|
180
|
+
.option("-l, --leverage <number>", "Maximum leverage allowed", "20")
|
|
181
|
+
.option("-d, --direction <direction>", "Trade direction: both, long, short", "both")
|
|
182
|
+
.action(async (crypto, options) => {
|
|
183
|
+
const style = ["conservative", "moderate", "aggressive"].includes(options.style)
|
|
184
|
+
? options.style
|
|
185
|
+
: "moderate";
|
|
186
|
+
const maxLeverage = Math.min(parseInt(options.leverage) || 20, 100);
|
|
187
|
+
const direction = ["both", "long", "short"].includes(options.direction)
|
|
188
|
+
? options.direction
|
|
189
|
+
: "both";
|
|
190
|
+
await (0, crypto_1.runStrategy)(crypto, { style, maxLeverage, direction });
|
|
191
|
+
});
|
|
192
|
+
// Order Block trading strategy
|
|
193
|
+
program
|
|
194
|
+
.command("orderblock <crypto>")
|
|
195
|
+
.alias("ob")
|
|
196
|
+
.description("Order Block trading strategy with BUY/SELL signals based on OB zones")
|
|
197
|
+
.option("-i, --interval <interval>", "Time interval (15m, 1h, 4h, 1d)", "4h")
|
|
198
|
+
.action(async (crypto, options) => {
|
|
199
|
+
const validIntervals = ["15m", "1h", "4h", "1d"];
|
|
200
|
+
const interval = validIntervals.includes(options.interval)
|
|
201
|
+
? options.interval
|
|
202
|
+
: "4h";
|
|
203
|
+
await (0, crypto_1.runOrderBlockStrategy)(crypto, interval);
|
|
204
|
+
});
|
|
205
|
+
// ICT (Inner Circle Trader) Strategy
|
|
206
|
+
program
|
|
207
|
+
.command("ict <crypto>")
|
|
208
|
+
.description("ICT trading strategy - Killzones, OTE, Breakers, Silver Bullet, AMD")
|
|
209
|
+
.option("-i, --interval <interval>", "Time interval (15m, 1h, 4h)", "1h")
|
|
210
|
+
.action(async (crypto, options) => {
|
|
211
|
+
const validIntervals = ["15m", "1h", "4h"];
|
|
212
|
+
const interval = validIntervals.includes(options.interval)
|
|
213
|
+
? options.interval
|
|
214
|
+
: "1h";
|
|
215
|
+
await (0, crypto_1.runICTStrategy)(crypto, interval);
|
|
216
|
+
});
|
|
147
217
|
// Model management commands
|
|
148
218
|
program
|
|
149
219
|
.command("model")
|
|
@@ -308,7 +378,12 @@ program.action(async () => {
|
|
|
308
378
|
default: "btc",
|
|
309
379
|
},
|
|
310
380
|
]);
|
|
311
|
-
|
|
381
|
+
try {
|
|
382
|
+
await (0, crypto_1.comprehensiveAnalysis)(cryptoSymbol);
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
// Error already handled in comprehensiveAnalysis
|
|
386
|
+
}
|
|
312
387
|
break;
|
|
313
388
|
}
|
|
314
389
|
case "signal": {
|
|
@@ -331,7 +406,148 @@ program.action(async () => {
|
|
|
331
406
|
{ name: "1 Day", value: "1d" },
|
|
332
407
|
],
|
|
333
408
|
});
|
|
334
|
-
|
|
409
|
+
try {
|
|
410
|
+
await (0, crypto_1.fullSignal)(cryptoSymbol, intervalChoice);
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
// Error already handled in fullSignal
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
case "whale": {
|
|
418
|
+
// Prompt for coins to track
|
|
419
|
+
const { coins } = await inquirer_1.default.prompt([
|
|
420
|
+
{
|
|
421
|
+
type: "input",
|
|
422
|
+
name: "coins",
|
|
423
|
+
message: "Enter coins to track (comma-separated):",
|
|
424
|
+
default: "BTC,ETH",
|
|
425
|
+
},
|
|
426
|
+
]);
|
|
427
|
+
const coinList = coins.split(",").map((c) => c.trim().toUpperCase());
|
|
428
|
+
await (0, crypto_1.runWhaleTracker)(coinList);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
case "scan": {
|
|
432
|
+
// Prompt for scan options
|
|
433
|
+
const scanLimit = await (0, select_1.default)({
|
|
434
|
+
message: "How many cryptocurrencies to scan?",
|
|
435
|
+
choices: [
|
|
436
|
+
{ name: "Top 20 (Quick)", value: 20 },
|
|
437
|
+
{ name: "Top 50 (Recommended)", value: 50 },
|
|
438
|
+
{ name: "Top 100 (Comprehensive)", value: 100 },
|
|
439
|
+
],
|
|
440
|
+
});
|
|
441
|
+
await (0, crypto_1.runMarketScanner)(scanLimit, 50);
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
case "news": {
|
|
445
|
+
// Prompt for optional coin filter
|
|
446
|
+
const newsFilterChoice = await (0, select_1.default)({
|
|
447
|
+
message: "Filter news by coin?",
|
|
448
|
+
choices: [
|
|
449
|
+
{ name: "All Crypto News", value: "" },
|
|
450
|
+
{ name: "Bitcoin (BTC)", value: "BTC" },
|
|
451
|
+
{ name: "Ethereum (ETH)", value: "ETH" },
|
|
452
|
+
{ name: "Solana (SOL)", value: "SOL" },
|
|
453
|
+
{ name: "Other (specify)", value: "__other__" },
|
|
454
|
+
],
|
|
455
|
+
});
|
|
456
|
+
let coinFilter;
|
|
457
|
+
if (newsFilterChoice === "__other__") {
|
|
458
|
+
const { customCoin } = await inquirer_1.default.prompt([
|
|
459
|
+
{
|
|
460
|
+
type: "input",
|
|
461
|
+
name: "customCoin",
|
|
462
|
+
message: "Enter coin symbol (e.g., XRP, ADA, DOGE):",
|
|
463
|
+
},
|
|
464
|
+
]);
|
|
465
|
+
coinFilter = customCoin.trim().toUpperCase() || undefined;
|
|
466
|
+
}
|
|
467
|
+
else if (newsFilterChoice) {
|
|
468
|
+
coinFilter = newsFilterChoice;
|
|
469
|
+
}
|
|
470
|
+
await (0, crypto_1.runCryptoNews)(coinFilter);
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
case "strategy": {
|
|
474
|
+
// Prompt for crypto symbol
|
|
475
|
+
const { strategySymbol } = await inquirer_1.default.prompt([
|
|
476
|
+
{
|
|
477
|
+
type: "input",
|
|
478
|
+
name: "strategySymbol",
|
|
479
|
+
message: "Enter cryptocurrency symbol (e.g., btc, eth, sol):",
|
|
480
|
+
default: "btc",
|
|
481
|
+
},
|
|
482
|
+
]);
|
|
483
|
+
// Prompt for trading style
|
|
484
|
+
const styleChoice = await (0, select_1.default)({
|
|
485
|
+
message: "Select your trading style:",
|
|
486
|
+
choices: [
|
|
487
|
+
{ name: "Conservative (5-10x leverage, wider stops)", value: "conservative" },
|
|
488
|
+
{ name: "Moderate (10-20x leverage, balanced) - Recommended", value: "moderate" },
|
|
489
|
+
{ name: "Aggressive (20-50x leverage, tighter stops)", value: "aggressive" },
|
|
490
|
+
],
|
|
491
|
+
});
|
|
492
|
+
// Prompt for direction
|
|
493
|
+
const directionChoice = await (0, select_1.default)({
|
|
494
|
+
message: "Trade direction:",
|
|
495
|
+
choices: [
|
|
496
|
+
{ name: "Both Long & Short", value: "both" },
|
|
497
|
+
{ name: "Long Only", value: "long" },
|
|
498
|
+
{ name: "Short Only", value: "short" },
|
|
499
|
+
],
|
|
500
|
+
});
|
|
501
|
+
await (0, crypto_1.runStrategy)(strategySymbol, {
|
|
502
|
+
style: styleChoice,
|
|
503
|
+
direction: directionChoice,
|
|
504
|
+
maxLeverage: styleChoice === "conservative" ? 10 : styleChoice === "moderate" ? 20 : 50,
|
|
505
|
+
});
|
|
506
|
+
break;
|
|
507
|
+
}
|
|
508
|
+
case "orderblock": {
|
|
509
|
+
// Prompt for crypto symbol
|
|
510
|
+
const { obSymbol } = await inquirer_1.default.prompt([
|
|
511
|
+
{
|
|
512
|
+
type: "input",
|
|
513
|
+
name: "obSymbol",
|
|
514
|
+
message: "Enter cryptocurrency symbol (e.g., btc, eth, sol):",
|
|
515
|
+
default: "btc",
|
|
516
|
+
},
|
|
517
|
+
]);
|
|
518
|
+
// Prompt for timeframe
|
|
519
|
+
const obIntervalChoice = await (0, select_1.default)({
|
|
520
|
+
message: "Select timeframe for Order Block analysis:",
|
|
521
|
+
choices: [
|
|
522
|
+
{ name: "4 Hours (Recommended for swing trades)", value: "4h" },
|
|
523
|
+
{ name: "1 Hour (Intraday trades)", value: "1h" },
|
|
524
|
+
{ name: "15 Minutes (Scalping)", value: "15m" },
|
|
525
|
+
{ name: "1 Day (Position trades)", value: "1d" },
|
|
526
|
+
],
|
|
527
|
+
});
|
|
528
|
+
await (0, crypto_1.runOrderBlockStrategy)(obSymbol, obIntervalChoice);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "ict": {
|
|
532
|
+
// Prompt for crypto symbol
|
|
533
|
+
const { ictSymbol } = await inquirer_1.default.prompt([
|
|
534
|
+
{
|
|
535
|
+
type: "input",
|
|
536
|
+
name: "ictSymbol",
|
|
537
|
+
message: "Enter cryptocurrency symbol (e.g., btc, eth, sol):",
|
|
538
|
+
default: "btc",
|
|
539
|
+
},
|
|
540
|
+
]);
|
|
541
|
+
// Prompt for timeframe
|
|
542
|
+
const ictIntervalChoice = await (0, select_1.default)({
|
|
543
|
+
message: "Select timeframe for ICT analysis:",
|
|
544
|
+
choices: [
|
|
545
|
+
{ name: "1 Hour (Recommended for ICT)", value: "1h" },
|
|
546
|
+
{ name: "15 Minutes (Scalping with Silver Bullet)", value: "15m" },
|
|
547
|
+
{ name: "4 Hours (Higher timeframe bias)", value: "4h" },
|
|
548
|
+
],
|
|
549
|
+
});
|
|
550
|
+
await (0, crypto_1.runICTStrategy)(ictSymbol, ictIntervalChoice);
|
|
335
551
|
break;
|
|
336
552
|
}
|
|
337
553
|
case "model": {
|
package/dist/lib/chat-handler.js
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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);
|
|
@@ -5,12 +5,15 @@
|
|
|
5
5
|
* Fetches OHLCV data from Binance public API (no API key required)
|
|
6
6
|
*/
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.fetchAllSymbols = fetchAllSymbols;
|
|
9
|
+
exports.findSimilarSymbols = findSimilarSymbols;
|
|
8
10
|
exports.normalizeSymbol = normalizeSymbol;
|
|
9
11
|
exports.fetchOHLCV = fetchOHLCV;
|
|
10
12
|
exports.fetch24hTicker = fetch24hTicker;
|
|
11
13
|
exports.fetchCryptoData = fetchCryptoData;
|
|
12
14
|
exports.getSupportedSymbols = getSupportedSymbols;
|
|
13
15
|
exports.isValidSymbol = isValidSymbol;
|
|
16
|
+
exports.validateSymbol = validateSymbol;
|
|
14
17
|
// Common crypto symbols mapping (user-friendly -> Binance format)
|
|
15
18
|
const SYMBOL_MAP = {
|
|
16
19
|
btc: "BTCUSDT",
|
|
@@ -57,6 +60,63 @@ const SYMBOL_MAP = {
|
|
|
57
60
|
wif: "WIFUSDT",
|
|
58
61
|
};
|
|
59
62
|
const BINANCE_API_BASE = "https://api.binance.com/api/v3";
|
|
63
|
+
// Cache for valid Binance symbols
|
|
64
|
+
let cachedSymbols = null;
|
|
65
|
+
let cacheTimestamp = 0;
|
|
66
|
+
const CACHE_DURATION = 1000 * 60 * 60; // 1 hour cache
|
|
67
|
+
/**
|
|
68
|
+
* Fetch all valid USDT trading pairs from Binance
|
|
69
|
+
*/
|
|
70
|
+
async function fetchAllSymbols() {
|
|
71
|
+
// Return cached symbols if still valid
|
|
72
|
+
if (cachedSymbols && Date.now() - cacheTimestamp < CACHE_DURATION) {
|
|
73
|
+
return cachedSymbols;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(`${BINANCE_API_BASE}/exchangeInfo`);
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
throw new Error("Failed to fetch exchange info");
|
|
79
|
+
}
|
|
80
|
+
const data = await response.json();
|
|
81
|
+
const symbols = new Set();
|
|
82
|
+
for (const symbol of data.symbols) {
|
|
83
|
+
// Only include USDT pairs that are trading
|
|
84
|
+
if (symbol.status === "TRADING" && symbol.quoteAsset === "USDT") {
|
|
85
|
+
symbols.add(symbol.symbol);
|
|
86
|
+
// Also add the base asset for easy lookup
|
|
87
|
+
symbols.add(symbol.baseAsset.toLowerCase());
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
cachedSymbols = symbols;
|
|
91
|
+
cacheTimestamp = Date.now();
|
|
92
|
+
return symbols;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
// Return empty set on error, will fall back to direct API check
|
|
96
|
+
return new Set();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get similar symbols for suggestions
|
|
101
|
+
*/
|
|
102
|
+
async function findSimilarSymbols(input, limit = 5) {
|
|
103
|
+
const symbols = await fetchAllSymbols();
|
|
104
|
+
const inputUpper = input.toUpperCase();
|
|
105
|
+
const similar = [];
|
|
106
|
+
for (const symbol of symbols) {
|
|
107
|
+
// Only show USDT pairs, not base assets
|
|
108
|
+
if (!symbol.endsWith("USDT"))
|
|
109
|
+
continue;
|
|
110
|
+
const baseAsset = symbol.replace("USDT", "");
|
|
111
|
+
// Check if base asset starts with or contains the input
|
|
112
|
+
if (baseAsset.startsWith(inputUpper) || baseAsset.includes(inputUpper)) {
|
|
113
|
+
similar.push(baseAsset);
|
|
114
|
+
if (similar.length >= limit)
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return similar;
|
|
119
|
+
}
|
|
60
120
|
/**
|
|
61
121
|
* Normalize symbol to Binance format
|
|
62
122
|
*/
|
|
@@ -156,6 +216,12 @@ function getSupportedSymbols() {
|
|
|
156
216
|
async function isValidSymbol(symbol) {
|
|
157
217
|
try {
|
|
158
218
|
const normalizedSymbol = normalizeSymbol(symbol);
|
|
219
|
+
// First check cache
|
|
220
|
+
const symbols = await fetchAllSymbols();
|
|
221
|
+
if (symbols.has(normalizedSymbol)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
// Fall back to direct API check
|
|
159
225
|
const url = `${BINANCE_API_BASE}/ticker/price?symbol=${normalizedSymbol}`;
|
|
160
226
|
const response = await fetch(url);
|
|
161
227
|
return response.ok;
|
|
@@ -164,3 +230,23 @@ async function isValidSymbol(symbol) {
|
|
|
164
230
|
return false;
|
|
165
231
|
}
|
|
166
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Validate symbol and get suggestions if invalid
|
|
235
|
+
*/
|
|
236
|
+
async function validateSymbol(symbol) {
|
|
237
|
+
const normalized = normalizeSymbol(symbol);
|
|
238
|
+
try {
|
|
239
|
+
const url = `${BINANCE_API_BASE}/ticker/price?symbol=${normalized}`;
|
|
240
|
+
const response = await fetch(url);
|
|
241
|
+
if (response.ok) {
|
|
242
|
+
return { valid: true, normalized, suggestions: [] };
|
|
243
|
+
}
|
|
244
|
+
// If invalid, find similar symbols
|
|
245
|
+
const suggestions = await findSimilarSymbols(symbol, 5);
|
|
246
|
+
return { valid: false, normalized, suggestions };
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
const suggestions = await findSimilarSymbols(symbol, 5);
|
|
250
|
+
return { valid: false, normalized, suggestions };
|
|
251
|
+
}
|
|
252
|
+
}
|