File size: 3,109 Bytes
dfe11f8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 | /**
* 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,
};
}
|