| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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; |
|
|
| |
| 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 }); |
| |
| 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 || {}; |
| } |
| } |
|
|
| |
| |
| |
| |
| export async function getSpotPrices() { |
| if (cache.data && Date.now() - cache.timestamp < CACHE_TTL_MS) { |
| return cache.data; |
| } |
| return refreshCache(); |
| } |
|
|
| |
| |
| |
| |
| export async function analyzeCryptoTarget(question) { |
| if (!question) return null; |
|
|
| |
| 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; |
|
|
| |
| 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, |
| }; |
| } |
|
|