Spaces:
Sleeping
Sleeping
Jose Salazar
Correccion de bugs del pipeline de IA, incorporo openrouter con deepseek para fallback, rate limit de login diferenciado entre prod y dev, cambios varios en UI
8a4b117 | /** | |
| * Cliente CoinGecko para spot prices de cripto. | |
| * | |
| * Sin API key (free tier publico). Rate limit ~30 req/min β usamos cache TTL 60s | |
| * para evitar excederlo cuando hay muchos mercados de cripto en un ciclo. | |
| * | |
| * Consumido por: | |
| * - signals/aiPipeline.js β enriquece el prompt con ground truth para | |
| * mercados de precio objetivo (BTC $150k, ETH $5k, etc). | |
| */ | |
| import { httpGet } from './httpClient.js'; | |
| import { logger } from './logger.js'; | |
| const COINGECKO_URL = 'https://api.coingecko.com/api/v3/simple/price'; | |
| const CACHE_TTL_MS = 60_000; | |
| // Mapeo symbol β id de CoinGecko | |
| const COIN_IDS = { | |
| BTC: 'bitcoin', | |
| ETH: 'ethereum', | |
| SOL: 'solana', | |
| DOGE: 'dogecoin', | |
| XRP: 'ripple', | |
| ADA: 'cardano', | |
| AVAX: 'avalanche-2', | |
| LINK: 'chainlink', | |
| MATIC: 'matic-network', | |
| DOT: 'polkadot', | |
| }; | |
| let cache = { data: null, timestamp: 0 }; | |
| async function refreshCache() { | |
| const ids = Object.values(COIN_IDS).join(','); | |
| const url = `${COINGECKO_URL}?ids=${ids}&vs_currencies=usd`; | |
| try { | |
| const data = await httpGet(url, { timeout: 8000, retries: 1 }); | |
| // Normaliza: { bitcoin: { usd: 103400 }, ... } β { BTC: 103400, ... } | |
| const normalized = {}; | |
| for (const [symbol, id] of Object.entries(COIN_IDS)) { | |
| if (data[id]?.usd != null) normalized[symbol] = data[id].usd; | |
| } | |
| cache = { data: normalized, timestamp: Date.now() }; | |
| return normalized; | |
| } catch (err) { | |
| logger.warn({ err: err.message }, 'CoinGecko fetch failed'); | |
| return cache.data || {}; | |
| } | |
| } | |
| /** | |
| * Devuelve { BTC: 103400, ETH: 3450, SOL: 142, ... } en USD. | |
| * Cache TTL 60s. | |
| */ | |
| export async function getSpotPrices() { | |
| if (cache.data && Date.now() - cache.timestamp < CACHE_TTL_MS) { | |
| return cache.data; | |
| } | |
| return refreshCache(); | |
| } | |
| /** | |
| * Detecta si una pregunta de mercado es de "precio objetivo cripto". | |
| * Devuelve { symbol, targetPrice, currentPrice, requiredMovePct, deadline } o null. | |
| */ | |
| export async function analyzeCryptoTarget(question) { | |
| if (!question) return null; | |
| // Detecta symbol (BTC|Bitcoin|ETH|Ethereum|SOL|Solana) | |
| const symbolMap = [ | |
| { rx: /\b(bitcoin|btc)\b/i, sym: 'BTC' }, | |
| { rx: /\b(ethereum|eth)\b/i, sym: 'ETH' }, | |
| { rx: /\b(solana|sol)\b/i, sym: 'SOL' }, | |
| { rx: /\b(dogecoin|doge)\b/i, sym: 'DOGE' }, | |
| { rx: /\b(xrp|ripple)\b/i, sym: 'XRP' }, | |
| { rx: /\b(cardano|ada)\b/i, sym: 'ADA' }, | |
| ]; | |
| const match = symbolMap.find(({ rx }) => rx.test(question)); | |
| if (!match) return null; | |
| // Detecta target price ($150k, $3,500, $100,000) | |
| const priceMatch = question.match(/\$\s?([\d,]+(?:\.\d+)?)\s?([kKmM])?/); | |
| if (!priceMatch) return null; | |
| let target = parseFloat(priceMatch[1].replace(/,/g, '')); | |
| if (priceMatch[2]?.toLowerCase() === 'k') target *= 1000; | |
| if (priceMatch[2]?.toLowerCase() === 'm') target *= 1_000_000; | |
| const spots = await getSpotPrices(); | |
| const current = spots[match.sym]; | |
| if (!current || !target) return null; | |
| const requiredMovePct = ((target - current) / current) * 100; | |
| return { | |
| symbol: match.sym, | |
| currentPrice: current, | |
| targetPrice: target, | |
| requiredMovePct, | |
| }; | |
| } | |