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,
  };
}