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.
@@ -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 Binance
253
+ * Fetch OHLCV candle data from Bybit (fallback)
86
254
  */
87
- async function fetchOHLCV(symbol, interval = "1h", limit = 100) {
255
+ async function fetchOHLCVFromBybit(symbol, interval = "1h", limit = 100) {
88
256
  const normalizedSymbol = normalizeSymbol(symbol);
89
- const url = `${BINANCE_API_BASE}/klines?symbol=${normalizedSymbol}&interval=${interval}&limit=${limit}`;
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
- // Binance klines format:
97
- // [0] Open time, [1] Open, [2] High, [3] Low, [4] Close, [5] Volume, ...
98
- return data.map((candle) => ({
99
- timestamp: candle[0],
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 for price change info
282
+ * Fetch 24h ticker data from Bybit (fallback)
109
283
  */
110
- async function fetch24hTicker(symbol) {
284
+ async function fetch24hTickerFromBybit(symbol) {
111
285
  const normalizedSymbol = normalizeSymbol(symbol);
112
- const url = `${BINANCE_API_BASE}/ticker/24hr?symbol=${normalizedSymbol}`;
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: parseFloat(data.lastPrice),
121
- priceChange: parseFloat(data.priceChange),
122
- priceChangePercent: parseFloat(data.priceChangePercent),
123
- high24h: parseFloat(data.highPrice),
124
- low24h: parseFloat(data.lowPrice),
125
- volume24h: parseFloat(data.volume),
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
- const url = `${BINANCE_API_BASE}/ticker/price?symbol=${normalizedSymbol}`;
160
- const response = await fetch(url);
161
- return response.ok;
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
+ }