prab-cli 1.2.5 → 1.2.7
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 +175 -2
- package/dist/lib/crypto/analyzer.js +397 -93
- package/dist/lib/crypto/data-fetcher.js +399 -21
- package/dist/lib/crypto/ict-strategy.js +955 -0
- package/dist/lib/crypto/index.js +24 -1
- package/dist/lib/crypto/orderblock-strategy.js +445 -0
- package/dist/lib/crypto/signal-generator.js +346 -10
- package/dist/lib/crypto/strategy-engine.js +803 -0
- package/dist/lib/crypto/strategy.js +673 -0
- package/dist/lib/slash-commands.js +24 -0
- package/dist/server/index.js +571 -0
- package/package.json +9 -3
|
@@ -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,171 @@ const SYMBOL_MAP = {
|
|
|
57
60
|
wif: "WIFUSDT",
|
|
58
61
|
};
|
|
59
62
|
const BINANCE_API_BASE = "https://api.binance.com/api/v3";
|
|
63
|
+
const BYBIT_API_BASE = "https://api.bybit.com/v5/market";
|
|
64
|
+
const CRYPTOCOMPARE_API_BASE = "https://min-api.cryptocompare.com/data";
|
|
65
|
+
// Map Binance intervals to Bybit intervals
|
|
66
|
+
const BYBIT_INTERVAL_MAP = {
|
|
67
|
+
"1m": "1",
|
|
68
|
+
"5m": "5",
|
|
69
|
+
"15m": "15",
|
|
70
|
+
"1h": "60",
|
|
71
|
+
"4h": "240",
|
|
72
|
+
"1d": "D",
|
|
73
|
+
"1w": "W",
|
|
74
|
+
};
|
|
75
|
+
// Map intervals to CryptoCompare endpoints and params
|
|
76
|
+
const CRYPTOCOMPARE_INTERVAL_MAP = {
|
|
77
|
+
"1m": { endpoint: "histominute", aggregate: 1 },
|
|
78
|
+
"5m": { endpoint: "histominute", aggregate: 5 },
|
|
79
|
+
"15m": { endpoint: "histominute", aggregate: 15 },
|
|
80
|
+
"1h": { endpoint: "histohour", aggregate: 1 },
|
|
81
|
+
"4h": { endpoint: "histohour", aggregate: 4 },
|
|
82
|
+
"1d": { endpoint: "histoday", aggregate: 1 },
|
|
83
|
+
"1w": { endpoint: "histoday", aggregate: 7 },
|
|
84
|
+
};
|
|
85
|
+
// Cache for valid Binance symbols
|
|
86
|
+
let cachedSymbols = null;
|
|
87
|
+
let cacheTimestamp = 0;
|
|
88
|
+
const CACHE_DURATION = 1000 * 60 * 60; // 1 hour cache
|
|
89
|
+
/**
|
|
90
|
+
* Fetch all valid USDT trading pairs from Bybit (fallback)
|
|
91
|
+
*/
|
|
92
|
+
async function fetchAllSymbolsFromBybit() {
|
|
93
|
+
try {
|
|
94
|
+
const response = await fetch(`${BYBIT_API_BASE}/instruments-info?category=spot`);
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
throw new Error("Failed to fetch exchange info from Bybit");
|
|
97
|
+
}
|
|
98
|
+
const data = await response.json();
|
|
99
|
+
const symbols = new Set();
|
|
100
|
+
if (data.retCode === 0 && data.result.list) {
|
|
101
|
+
for (const instrument of data.result.list) {
|
|
102
|
+
// Only include USDT pairs that are trading
|
|
103
|
+
if (instrument.status === "Trading" && instrument.quoteCoin === "USDT") {
|
|
104
|
+
symbols.add(instrument.symbol);
|
|
105
|
+
// Also add the base asset for easy lookup
|
|
106
|
+
symbols.add(instrument.baseCoin.toLowerCase());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return symbols;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return new Set();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get common crypto symbols as fallback (when all APIs are blocked)
|
|
118
|
+
*/
|
|
119
|
+
function getCommonSymbols() {
|
|
120
|
+
const common = [
|
|
121
|
+
"BTC",
|
|
122
|
+
"ETH",
|
|
123
|
+
"BNB",
|
|
124
|
+
"XRP",
|
|
125
|
+
"SOL",
|
|
126
|
+
"ADA",
|
|
127
|
+
"DOGE",
|
|
128
|
+
"DOT",
|
|
129
|
+
"MATIC",
|
|
130
|
+
"LTC",
|
|
131
|
+
"AVAX",
|
|
132
|
+
"LINK",
|
|
133
|
+
"ATOM",
|
|
134
|
+
"UNI",
|
|
135
|
+
"XLM",
|
|
136
|
+
"ALGO",
|
|
137
|
+
"NEAR",
|
|
138
|
+
"APT",
|
|
139
|
+
"ARB",
|
|
140
|
+
"OP",
|
|
141
|
+
"SUI",
|
|
142
|
+
"PEPE",
|
|
143
|
+
"SHIB",
|
|
144
|
+
"WIF",
|
|
145
|
+
"TRX",
|
|
146
|
+
"ETC",
|
|
147
|
+
"FIL",
|
|
148
|
+
"HBAR",
|
|
149
|
+
"VET",
|
|
150
|
+
"ICP",
|
|
151
|
+
];
|
|
152
|
+
const symbols = new Set();
|
|
153
|
+
for (const sym of common) {
|
|
154
|
+
symbols.add(`${sym}USDT`);
|
|
155
|
+
symbols.add(sym.toLowerCase());
|
|
156
|
+
}
|
|
157
|
+
return symbols;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Fetch all valid USDT trading pairs (tries Binance -> Bybit -> fallback to common)
|
|
161
|
+
*/
|
|
162
|
+
async function fetchAllSymbols() {
|
|
163
|
+
// Return cached symbols if still valid
|
|
164
|
+
if (cachedSymbols && Date.now() - cacheTimestamp < CACHE_DURATION) {
|
|
165
|
+
return cachedSymbols;
|
|
166
|
+
}
|
|
167
|
+
// Try Binance first
|
|
168
|
+
try {
|
|
169
|
+
const response = await fetch(`${BINANCE_API_BASE}/exchangeInfo`);
|
|
170
|
+
if (response.ok) {
|
|
171
|
+
const data = await response.json();
|
|
172
|
+
const symbols = new Set();
|
|
173
|
+
for (const symbol of data.symbols) {
|
|
174
|
+
// Only include USDT pairs that are trading
|
|
175
|
+
if (symbol.status === "TRADING" && symbol.quoteAsset === "USDT") {
|
|
176
|
+
symbols.add(symbol.symbol);
|
|
177
|
+
// Also add the base asset for easy lookup
|
|
178
|
+
symbols.add(symbol.baseAsset.toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
cachedSymbols = symbols;
|
|
182
|
+
cacheTimestamp = Date.now();
|
|
183
|
+
return symbols;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// Continue to fallback
|
|
188
|
+
}
|
|
189
|
+
// Try Bybit as fallback
|
|
190
|
+
try {
|
|
191
|
+
const symbols = await fetchAllSymbolsFromBybit();
|
|
192
|
+
if (symbols.size > 0) {
|
|
193
|
+
cachedSymbols = symbols;
|
|
194
|
+
cacheTimestamp = Date.now();
|
|
195
|
+
return symbols;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// Continue to fallback
|
|
200
|
+
}
|
|
201
|
+
// Use common symbols as final fallback
|
|
202
|
+
const symbols = getCommonSymbols();
|
|
203
|
+
cachedSymbols = symbols;
|
|
204
|
+
cacheTimestamp = Date.now();
|
|
205
|
+
return symbols;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get similar symbols for suggestions
|
|
209
|
+
*/
|
|
210
|
+
async function findSimilarSymbols(input, limit = 5) {
|
|
211
|
+
const symbols = await fetchAllSymbols();
|
|
212
|
+
const inputUpper = input.toUpperCase();
|
|
213
|
+
const similar = [];
|
|
214
|
+
for (const symbol of symbols) {
|
|
215
|
+
// Only show USDT pairs, not base assets
|
|
216
|
+
if (!symbol.endsWith("USDT"))
|
|
217
|
+
continue;
|
|
218
|
+
const baseAsset = symbol.replace("USDT", "");
|
|
219
|
+
// Check if base asset starts with or contains the input
|
|
220
|
+
if (baseAsset.startsWith(inputUpper) || baseAsset.includes(inputUpper)) {
|
|
221
|
+
similar.push(baseAsset);
|
|
222
|
+
if (similar.length >= limit)
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return similar;
|
|
227
|
+
}
|
|
60
228
|
/**
|
|
61
229
|
* Normalize symbol to Binance format
|
|
62
230
|
*/
|
|
@@ -82,49 +250,233 @@ function normalizeSymbol(input) {
|
|
|
82
250
|
return lower.toUpperCase() + "USDT";
|
|
83
251
|
}
|
|
84
252
|
/**
|
|
85
|
-
* Fetch OHLCV candle data from
|
|
253
|
+
* Fetch OHLCV candle data from Bybit (fallback)
|
|
86
254
|
*/
|
|
87
|
-
async function
|
|
255
|
+
async function fetchOHLCVFromBybit(symbol, interval = "1h", limit = 100) {
|
|
88
256
|
const normalizedSymbol = normalizeSymbol(symbol);
|
|
89
|
-
const
|
|
257
|
+
const bybitInterval = BYBIT_INTERVAL_MAP[interval];
|
|
258
|
+
const url = `${BYBIT_API_BASE}/kline?category=spot&symbol=${normalizedSymbol}&interval=${bybitInterval}&limit=${limit}`;
|
|
90
259
|
const response = await fetch(url);
|
|
91
260
|
if (!response.ok) {
|
|
92
261
|
const error = await response.text();
|
|
93
262
|
throw new Error(`Failed to fetch data for ${normalizedSymbol}: ${error}`);
|
|
94
263
|
}
|
|
95
264
|
const data = await response.json();
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
265
|
+
if (data.retCode !== 0) {
|
|
266
|
+
throw new Error(`Bybit API error: ${data.retMsg}`);
|
|
267
|
+
}
|
|
268
|
+
// Bybit klines are in reverse order (newest first), so we reverse them
|
|
269
|
+
// Format: [startTime, openPrice, highPrice, lowPrice, closePrice, volume, turnover]
|
|
270
|
+
return data.result.list
|
|
271
|
+
.map((candle) => ({
|
|
272
|
+
timestamp: parseInt(candle[0]),
|
|
100
273
|
open: parseFloat(candle[1]),
|
|
101
274
|
high: parseFloat(candle[2]),
|
|
102
275
|
low: parseFloat(candle[3]),
|
|
103
276
|
close: parseFloat(candle[4]),
|
|
104
277
|
volume: parseFloat(candle[5]),
|
|
105
|
-
}))
|
|
278
|
+
}))
|
|
279
|
+
.reverse();
|
|
106
280
|
}
|
|
107
281
|
/**
|
|
108
|
-
* Fetch 24h ticker data
|
|
282
|
+
* Fetch 24h ticker data from Bybit (fallback)
|
|
109
283
|
*/
|
|
110
|
-
async function
|
|
284
|
+
async function fetch24hTickerFromBybit(symbol) {
|
|
111
285
|
const normalizedSymbol = normalizeSymbol(symbol);
|
|
112
|
-
const url = `${
|
|
286
|
+
const url = `${BYBIT_API_BASE}/tickers?category=spot&symbol=${normalizedSymbol}`;
|
|
113
287
|
const response = await fetch(url);
|
|
114
288
|
if (!response.ok) {
|
|
115
289
|
const error = await response.text();
|
|
116
290
|
throw new Error(`Failed to fetch ticker for ${normalizedSymbol}: ${error}`);
|
|
117
291
|
}
|
|
118
292
|
const data = await response.json();
|
|
293
|
+
if (data.retCode !== 0 || !data.result.list.length) {
|
|
294
|
+
throw new Error(`Bybit API error: ${data.retMsg || "Symbol not found"}`);
|
|
295
|
+
}
|
|
296
|
+
const ticker = data.result.list[0];
|
|
297
|
+
const price = parseFloat(ticker.lastPrice);
|
|
298
|
+
const prevPrice = parseFloat(ticker.prevPrice24h);
|
|
299
|
+
const priceChange = price - prevPrice;
|
|
300
|
+
const priceChangePercent = parseFloat(ticker.price24hPcnt) * 100;
|
|
301
|
+
return {
|
|
302
|
+
price,
|
|
303
|
+
priceChange,
|
|
304
|
+
priceChangePercent,
|
|
305
|
+
high24h: parseFloat(ticker.highPrice24h),
|
|
306
|
+
low24h: parseFloat(ticker.lowPrice24h),
|
|
307
|
+
volume24h: parseFloat(ticker.volume24h),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Check if error is due to geo-restriction or CloudFront block
|
|
312
|
+
*/
|
|
313
|
+
function isGeoRestricted(error) {
|
|
314
|
+
return (error.includes("restricted location") ||
|
|
315
|
+
error.includes("Service unavailable") ||
|
|
316
|
+
error.includes("CloudFront") ||
|
|
317
|
+
error.includes("403") ||
|
|
318
|
+
error.includes("block access from your country"));
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Extract base symbol from normalized symbol (e.g., BTCUSDT -> BTC)
|
|
322
|
+
*/
|
|
323
|
+
function getBaseSymbol(normalizedSymbol) {
|
|
324
|
+
if (normalizedSymbol.endsWith("USDT")) {
|
|
325
|
+
return normalizedSymbol.slice(0, -4);
|
|
326
|
+
}
|
|
327
|
+
if (normalizedSymbol.endsWith("USD")) {
|
|
328
|
+
return normalizedSymbol.slice(0, -3);
|
|
329
|
+
}
|
|
330
|
+
return normalizedSymbol;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Fetch OHLCV candle data from CryptoCompare (globally accessible, no geo-restrictions)
|
|
334
|
+
*/
|
|
335
|
+
async function fetchOHLCVFromCryptoCompare(symbol, interval = "1h", limit = 100) {
|
|
336
|
+
const normalizedSymbol = normalizeSymbol(symbol);
|
|
337
|
+
const baseSymbol = getBaseSymbol(normalizedSymbol);
|
|
338
|
+
const { endpoint, aggregate } = CRYPTOCOMPARE_INTERVAL_MAP[interval];
|
|
339
|
+
const url = `${CRYPTOCOMPARE_API_BASE}/v2/${endpoint}?fsym=${baseSymbol}&tsym=USDT&limit=${limit}&aggregate=${aggregate}`;
|
|
340
|
+
const response = await fetch(url);
|
|
341
|
+
if (!response.ok) {
|
|
342
|
+
const error = await response.text();
|
|
343
|
+
throw new Error(`CryptoCompare error for ${baseSymbol}: ${error}`);
|
|
344
|
+
}
|
|
345
|
+
const data = await response.json();
|
|
346
|
+
if (data.Response === "Error") {
|
|
347
|
+
throw new Error(`CryptoCompare error: ${data.Message}`);
|
|
348
|
+
}
|
|
349
|
+
// CryptoCompare format: { time, open, high, low, close, volumefrom, volumeto }
|
|
350
|
+
return data.Data.Data.map((candle) => ({
|
|
351
|
+
timestamp: candle.time * 1000, // Convert to milliseconds
|
|
352
|
+
open: candle.open,
|
|
353
|
+
high: candle.high,
|
|
354
|
+
low: candle.low,
|
|
355
|
+
close: candle.close,
|
|
356
|
+
volume: candle.volumefrom,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Fetch 24h ticker data from CryptoCompare (globally accessible)
|
|
361
|
+
*/
|
|
362
|
+
async function fetch24hTickerFromCryptoCompare(symbol) {
|
|
363
|
+
const normalizedSymbol = normalizeSymbol(symbol);
|
|
364
|
+
const baseSymbol = getBaseSymbol(normalizedSymbol);
|
|
365
|
+
// Fetch current price and 24h data
|
|
366
|
+
const [priceResponse, dayResponse] = await Promise.all([
|
|
367
|
+
fetch(`${CRYPTOCOMPARE_API_BASE}/price?fsym=${baseSymbol}&tsyms=USDT`),
|
|
368
|
+
fetch(`${CRYPTOCOMPARE_API_BASE}/v2/histoday?fsym=${baseSymbol}&tsym=USDT&limit=1`),
|
|
369
|
+
]);
|
|
370
|
+
if (!priceResponse.ok || !dayResponse.ok) {
|
|
371
|
+
throw new Error(`CryptoCompare error fetching ticker for ${baseSymbol}`);
|
|
372
|
+
}
|
|
373
|
+
const priceData = await priceResponse.json();
|
|
374
|
+
const dayData = await dayResponse.json();
|
|
375
|
+
if (priceData.Response === "Error" || dayData.Response === "Error") {
|
|
376
|
+
throw new Error(`CryptoCompare error: Symbol ${baseSymbol} not found`);
|
|
377
|
+
}
|
|
378
|
+
const currentPrice = priceData.USDT;
|
|
379
|
+
const dayCandles = dayData.Data.Data;
|
|
380
|
+
// Get yesterday's data for 24h change calculation
|
|
381
|
+
const yesterday = dayCandles[0];
|
|
382
|
+
const today = dayCandles[1] || yesterday;
|
|
383
|
+
const priceChange = currentPrice - yesterday.close;
|
|
384
|
+
const priceChangePercent = (priceChange / yesterday.close) * 100;
|
|
119
385
|
return {
|
|
120
|
-
price:
|
|
121
|
-
priceChange
|
|
122
|
-
priceChangePercent
|
|
123
|
-
high24h:
|
|
124
|
-
low24h:
|
|
125
|
-
volume24h:
|
|
386
|
+
price: currentPrice,
|
|
387
|
+
priceChange,
|
|
388
|
+
priceChangePercent,
|
|
389
|
+
high24h: today.high,
|
|
390
|
+
low24h: today.low,
|
|
391
|
+
volume24h: today.volumefrom,
|
|
126
392
|
};
|
|
127
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Fetch OHLCV candle data (tries Binance -> Bybit -> CryptoCompare)
|
|
396
|
+
*/
|
|
397
|
+
async function fetchOHLCV(symbol, interval = "1h", limit = 100) {
|
|
398
|
+
const normalizedSymbol = normalizeSymbol(symbol);
|
|
399
|
+
const errors = [];
|
|
400
|
+
// Try Binance first
|
|
401
|
+
try {
|
|
402
|
+
const url = `${BINANCE_API_BASE}/klines?symbol=${normalizedSymbol}&interval=${interval}&limit=${limit}`;
|
|
403
|
+
const response = await fetch(url);
|
|
404
|
+
if (response.ok) {
|
|
405
|
+
const data = await response.json();
|
|
406
|
+
// Binance klines format:
|
|
407
|
+
// [0] Open time, [1] Open, [2] High, [3] Low, [4] Close, [5] Volume, ...
|
|
408
|
+
return data.map((candle) => ({
|
|
409
|
+
timestamp: candle[0],
|
|
410
|
+
open: parseFloat(candle[1]),
|
|
411
|
+
high: parseFloat(candle[2]),
|
|
412
|
+
low: parseFloat(candle[3]),
|
|
413
|
+
close: parseFloat(candle[4]),
|
|
414
|
+
volume: parseFloat(candle[5]),
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
errors.push(`Binance: ${response.status}`);
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
errors.push(`Binance: ${e.message}`);
|
|
421
|
+
}
|
|
422
|
+
// Try Bybit as fallback
|
|
423
|
+
try {
|
|
424
|
+
return await fetchOHLCVFromBybit(symbol, interval, limit);
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
errors.push(`Bybit: ${e.message}`);
|
|
428
|
+
}
|
|
429
|
+
// Try CryptoCompare as final fallback (globally accessible)
|
|
430
|
+
try {
|
|
431
|
+
return await fetchOHLCVFromCryptoCompare(symbol, interval, limit);
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
errors.push(`CryptoCompare: ${e.message}`);
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`Failed to fetch data for ${normalizedSymbol}. Tried all providers: ${errors.join("; ")}`);
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Fetch 24h ticker data for price change info (tries Binance -> Bybit -> CryptoCompare)
|
|
440
|
+
*/
|
|
441
|
+
async function fetch24hTicker(symbol) {
|
|
442
|
+
const normalizedSymbol = normalizeSymbol(symbol);
|
|
443
|
+
const errors = [];
|
|
444
|
+
// Try Binance first
|
|
445
|
+
try {
|
|
446
|
+
const url = `${BINANCE_API_BASE}/ticker/24hr?symbol=${normalizedSymbol}`;
|
|
447
|
+
const response = await fetch(url);
|
|
448
|
+
if (response.ok) {
|
|
449
|
+
const data = await response.json();
|
|
450
|
+
return {
|
|
451
|
+
price: parseFloat(data.lastPrice),
|
|
452
|
+
priceChange: parseFloat(data.priceChange),
|
|
453
|
+
priceChangePercent: parseFloat(data.priceChangePercent),
|
|
454
|
+
high24h: parseFloat(data.highPrice),
|
|
455
|
+
low24h: parseFloat(data.lowPrice),
|
|
456
|
+
volume24h: parseFloat(data.volume),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
errors.push(`Binance: ${response.status}`);
|
|
460
|
+
}
|
|
461
|
+
catch (e) {
|
|
462
|
+
errors.push(`Binance: ${e.message}`);
|
|
463
|
+
}
|
|
464
|
+
// Try Bybit as fallback
|
|
465
|
+
try {
|
|
466
|
+
return await fetch24hTickerFromBybit(symbol);
|
|
467
|
+
}
|
|
468
|
+
catch (e) {
|
|
469
|
+
errors.push(`Bybit: ${e.message}`);
|
|
470
|
+
}
|
|
471
|
+
// Try CryptoCompare as final fallback (globally accessible)
|
|
472
|
+
try {
|
|
473
|
+
return await fetch24hTickerFromCryptoCompare(symbol);
|
|
474
|
+
}
|
|
475
|
+
catch (e) {
|
|
476
|
+
errors.push(`CryptoCompare: ${e.message}`);
|
|
477
|
+
}
|
|
478
|
+
throw new Error(`Failed to fetch ticker for ${normalizedSymbol}. Tried all providers: ${errors.join("; ")}`);
|
|
479
|
+
}
|
|
128
480
|
/**
|
|
129
481
|
* Fetch complete crypto data for analysis
|
|
130
482
|
*/
|
|
@@ -151,16 +503,42 @@ function getSupportedSymbols() {
|
|
|
151
503
|
return [...new Set(Object.keys(SYMBOL_MAP))];
|
|
152
504
|
}
|
|
153
505
|
/**
|
|
154
|
-
* Check if a symbol is valid/supported
|
|
506
|
+
* Check if a symbol is valid/supported by trying to fetch its price
|
|
155
507
|
*/
|
|
156
508
|
async function isValidSymbol(symbol) {
|
|
157
509
|
try {
|
|
158
510
|
const normalizedSymbol = normalizeSymbol(symbol);
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
511
|
+
// First check our symbol cache
|
|
512
|
+
const symbols = await fetchAllSymbols();
|
|
513
|
+
if (symbols.has(normalizedSymbol) || symbols.has(normalizedSymbol.toLowerCase())) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
// Try to actually fetch the ticker to validate
|
|
517
|
+
try {
|
|
518
|
+
await fetch24hTicker(symbol);
|
|
519
|
+
return true;
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
162
524
|
}
|
|
163
525
|
catch {
|
|
164
526
|
return false;
|
|
165
527
|
}
|
|
166
528
|
}
|
|
529
|
+
/**
|
|
530
|
+
* Validate symbol and get suggestions if invalid
|
|
531
|
+
*/
|
|
532
|
+
async function validateSymbol(symbol) {
|
|
533
|
+
const normalized = normalizeSymbol(symbol);
|
|
534
|
+
try {
|
|
535
|
+
// Try to fetch the ticker - this will try all providers
|
|
536
|
+
await fetch24hTicker(symbol);
|
|
537
|
+
return { valid: true, normalized, suggestions: [] };
|
|
538
|
+
}
|
|
539
|
+
catch {
|
|
540
|
+
// If invalid, find similar symbols from our cache
|
|
541
|
+
const suggestions = await findSimilarSymbols(symbol, 5);
|
|
542
|
+
return { valid: false, normalized, suggestions };
|
|
543
|
+
}
|
|
544
|
+
}
|