|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { API_REGISTRY, getTotalEndpointsCount } from './api-registry.js';
|
|
|
|
|
|
export class RealDataFetcher {
|
|
|
constructor() {
|
|
|
this.failedProviders = new Map();
|
|
|
this.providerStats = new Map();
|
|
|
this.cache = new Map();
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchMarketData(limit = 50) {
|
|
|
const providers = [
|
|
|
{ name: 'CoinGecko', fetcher: () => this.fetchFromCoinGecko(limit) },
|
|
|
{ name: 'Binance', fetcher: () => this.fetchFromBinance(limit) },
|
|
|
{ name: 'CoinMarketCap', fetcher: () => this.fetchFromCoinMarketCap(limit) }
|
|
|
];
|
|
|
return this.tryProviders(providers, 'market_data');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchTrendingCoins() {
|
|
|
const providers = [
|
|
|
{ name: 'CoinGecko Trending', fetcher: () => this.fetchCoinGeckoTrending() },
|
|
|
{ name: 'CoinCap Top', fetcher: () => this.fetchCoinCapTop() }
|
|
|
];
|
|
|
return this.tryProviders(providers, 'trending');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchSentimentData(timeframe = '1D') {
|
|
|
const providers = [
|
|
|
{ name: 'Fear & Greed', fetcher: () => this.fetchFearGreedIndex() },
|
|
|
{ name: 'LunarCrush', fetcher: () => this.fetchLunarCrushSentiment() }
|
|
|
];
|
|
|
return this.tryProviders(providers, 'sentiment');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchOnChainAnalytics() {
|
|
|
const providers = [
|
|
|
{ name: 'Glassnode', fetcher: () => this.fetchGlassnodeData() },
|
|
|
{ name: 'Covalent', fetcher: () => this.fetchCovalentData() }
|
|
|
];
|
|
|
return this.tryProviders(providers, 'onchain');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchLatestNews(query = 'cryptocurrency') {
|
|
|
const providers = [
|
|
|
{ name: 'NewsAPI', fetcher: () => this.fetchNewsAPI(query) },
|
|
|
{ name: 'CryptoPanic', fetcher: () => this.fetchCryptoPanic() }
|
|
|
];
|
|
|
return this.tryProviders(providers, 'news');
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async tryProviders(providers, category) {
|
|
|
for (const provider of providers) {
|
|
|
try {
|
|
|
console.log(`[RealDataFetcher] Trying ${provider.name}...`);
|
|
|
const data = await provider.fetcher();
|
|
|
if (data) {
|
|
|
console.log(`[RealDataFetcher] ✅ ${provider.name} succeeded`);
|
|
|
this.recordProviderSuccess(provider.name);
|
|
|
return data;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.warn(`[RealDataFetcher] ❌ ${provider.name} failed:`, error.message);
|
|
|
this.recordProviderFailure(provider.name);
|
|
|
}
|
|
|
}
|
|
|
console.error('[RealDataFetcher] All providers failed for', category);
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFromCoinGecko(limit = 50) {
|
|
|
try {
|
|
|
const url = `https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=${Math.min(limit, 250)}&sparkline=true&price_change_percentage=7d`;
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
coins: data.map(coin => ({
|
|
|
rank: coin.market_cap_rank,
|
|
|
name: coin.name,
|
|
|
symbol: coin.symbol.toUpperCase(),
|
|
|
price: coin.current_price,
|
|
|
volume_24h: coin.total_volume,
|
|
|
market_cap: coin.market_cap,
|
|
|
change_24h: coin.price_change_percentage_24h,
|
|
|
change_7d: coin.price_change_percentage_7d_in_currency,
|
|
|
image: coin.image
|
|
|
})),
|
|
|
timestamp: new Date().toISOString(),
|
|
|
source: 'coingecko'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[CoinGecko] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async fetchCoinGeckoTrending() {
|
|
|
try {
|
|
|
const url = 'https://api.coingecko.com/api/v3/search/trending';
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
coins: data.coins.slice(0, 10).map((item, i) => ({
|
|
|
rank: i + 1,
|
|
|
name: item.item.name,
|
|
|
symbol: item.item.symbol.toUpperCase(),
|
|
|
price: item.item.data.price,
|
|
|
market_cap: item.item.data.market_cap,
|
|
|
change_24h: item.item.data.price_change_percentage_24h,
|
|
|
image: item.item.large
|
|
|
})),
|
|
|
source: 'coingecko_trending'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[CoinGecko Trending] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async fetchGlobalMarketData() {
|
|
|
try {
|
|
|
const url = 'https://api.coingecko.com/api/v3/global';
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
total_market_cap: data.data.total_market_cap.usd,
|
|
|
total_volume: data.data.total_24h_vol.usd,
|
|
|
btc_dominance: data.data.btc_dominance,
|
|
|
active_cryptocurrencies: data.data.active_cryptocurrencies
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[CoinGecko Global] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFromBinance(limit = 50) {
|
|
|
try {
|
|
|
const url = 'https://api.binance.com/api/v3/ticker/24hr';
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
|
|
|
return {
|
|
|
coins: data.slice(0, limit).map((ticker, i) => ({
|
|
|
rank: i + 1,
|
|
|
symbol: ticker.symbol.replace('USDT', ''),
|
|
|
price: parseFloat(ticker.lastPrice),
|
|
|
volume_24h: parseFloat(ticker.volume),
|
|
|
change_24h: parseFloat(ticker.priceChangePercent)
|
|
|
})),
|
|
|
source: 'binance'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[Binance] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFromCoinMarketCap(limit = 50) {
|
|
|
try {
|
|
|
|
|
|
const key = API_REGISTRY.market.coinmarketcap.key;
|
|
|
if (!key) throw new Error('CoinMarketCap key not configured');
|
|
|
|
|
|
const url = `https://pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest?limit=${limit}&convert=USD`;
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
headers: {
|
|
|
'X-CMC_PRO_API_KEY': key
|
|
|
}
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
coins: data.data.map((coin, i) => ({
|
|
|
rank: coin.cmc_rank,
|
|
|
name: coin.name,
|
|
|
symbol: coin.symbol,
|
|
|
price: coin.quote.USD.price,
|
|
|
volume_24h: coin.quote.USD.volume_24h,
|
|
|
market_cap: coin.quote.USD.market_cap,
|
|
|
change_24h: coin.quote.USD.percent_change_24h
|
|
|
})),
|
|
|
source: 'coinmarketcap'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[CoinMarketCap] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchCoinCapTop() {
|
|
|
try {
|
|
|
const url = 'https://api.coincap.io/v2/assets?limit=50';
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
coins: data.data.map((coin, i) => ({
|
|
|
rank: parseInt(coin.rank),
|
|
|
name: coin.name,
|
|
|
symbol: coin.symbol,
|
|
|
price: parseFloat(coin.priceUsd),
|
|
|
volume_24h: parseFloat(coin.volumeUsd24Hr),
|
|
|
market_cap: parseFloat(coin.marketCapUsd),
|
|
|
change_24h: parseFloat(coin.changePercent24Hr)
|
|
|
})),
|
|
|
source: 'coincap'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[CoinCap] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchFearGreedIndex() {
|
|
|
try {
|
|
|
const url = 'https://api.alternative.me/fng/?limit=30';
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
current: data.data[0],
|
|
|
history: data.data,
|
|
|
source: 'fear_greed'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[Fear & Greed] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async fetchLunarCrushSentiment() {
|
|
|
try {
|
|
|
|
|
|
throw new Error('LunarCrush requires API key');
|
|
|
} catch (error) {
|
|
|
console.error('[LunarCrush] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchGlassnodeData() {
|
|
|
try {
|
|
|
|
|
|
throw new Error('Glassnode requires API key');
|
|
|
} catch (error) {
|
|
|
console.error('[Glassnode] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async fetchCovalentData() {
|
|
|
try {
|
|
|
|
|
|
throw new Error('Covalent requires API key');
|
|
|
} catch (error) {
|
|
|
console.error('[Covalent] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async fetchNewsAPI(query = 'cryptocurrency') {
|
|
|
try {
|
|
|
const key = 'pub_346789abc123def456789ghi012345jkl';
|
|
|
const url = `https://newsapi.org/v2/everything?q=${query}&sortBy=publishedAt&language=en&pageSize=50&apiKey=${key}`;
|
|
|
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
articles: data.articles.slice(0, 50).map(article => ({
|
|
|
title: article.title,
|
|
|
description: article.description,
|
|
|
url: article.url,
|
|
|
source: article.source.name,
|
|
|
published_at: article.publishedAt,
|
|
|
image: article.urlToImage
|
|
|
})),
|
|
|
source: 'newsapi'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[NewsAPI] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async fetchCryptoPanic() {
|
|
|
try {
|
|
|
const url = 'https://cryptopanic.com/api/v1/posts/?auth_token=optional&limit=50';
|
|
|
const response = await fetch(url);
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
|
|
|
|
const data = await response.json();
|
|
|
return {
|
|
|
articles: data.results.slice(0, 50).map(article => ({
|
|
|
title: article.title,
|
|
|
url: article.link,
|
|
|
source: article.source.title,
|
|
|
kind: article.kind,
|
|
|
published_at: article.published_at
|
|
|
})),
|
|
|
source: 'cryptopanic'
|
|
|
};
|
|
|
} catch (error) {
|
|
|
console.error('[CryptoPanic] Error:', error);
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
recordProviderSuccess(providerName) {
|
|
|
const stats = this.providerStats.get(providerName) || { success: 0, failures: 0 };
|
|
|
stats.success++;
|
|
|
this.providerStats.set(providerName, stats);
|
|
|
|
|
|
|
|
|
this.failedProviders.delete(providerName);
|
|
|
}
|
|
|
|
|
|
recordProviderFailure(providerName) {
|
|
|
const stats = this.providerStats.get(providerName) || { success: 0, failures: 0 };
|
|
|
stats.failures++;
|
|
|
this.providerStats.set(providerName, stats);
|
|
|
|
|
|
|
|
|
const failures = (this.failedProviders.get(providerName) || 0) + 1;
|
|
|
this.failedProviders.set(providerName, failures);
|
|
|
}
|
|
|
|
|
|
getProviderStats() {
|
|
|
return Object.fromEntries(this.providerStats);
|
|
|
}
|
|
|
|
|
|
getTotalEndpoints() {
|
|
|
return getTotalEndpointsCount();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
export const realDataFetcher = new RealDataFetcher();
|
|
|
export default realDataFetcher;
|
|
|
|