Jose Salazar commited on
Commit
eb2ad03
Β·
1 Parent(s): bee0110

Integro docs faltantes de la rama de franco

Browse files
backend/docs/FINNHUB.md ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FINNHUB.md β€” Modulo de noticias financieras
2
+
3
+ Fuente de titulares financieros para el pipeline de IA. Consulta la API REST de Finnhub (free tier) y proporciona noticias filtradas por relevancia al mercado analizado.
4
+
5
+ **No expone endpoints REST publicos.** Es un modulo interno consumido unicamente por `signals/aiPipeline.js` durante la generacion de senales (cada 5 min).
6
+
7
+ ---
8
+
9
+ ## Variables de entorno
10
+
11
+ | Variable | Obligatoria | Descripcion |
12
+ |---|---|---|
13
+ | `FINNHUB_API_KEY` | No | API key de Finnhub (finnhub.io). Sin ella el modulo devuelve array vacio y el pipeline usa rule-based. |
14
+
15
+ ---
16
+
17
+ ## API interna
18
+
19
+ ### `getMarketNews(category, limit)`
20
+
21
+ **Archivo:** `src/finnhub/finnhub.client.js`
22
+
23
+ Obtiene noticias generales del mercado por categoria.
24
+
25
+ **Parametros**
26
+
27
+ | Param | Tipo | Default | Descripcion |
28
+ |---|---|---|---|
29
+ | `category` | string | `"general"` | Categoria: `general`, `forex`, `crypto`, `merger` |
30
+ | `limit` | int | `50` | Maximo de noticias a devolver |
31
+
32
+ **Retorno:** `Promise<Object[]>` β€” array de noticias normalizadas.
33
+
34
+ **Noticia normalizada:**
35
+
36
+ ```json
37
+ {
38
+ "id": 12345,
39
+ "headline": "Fed signals potential rate cut...",
40
+ "summary": "The Federal Reserve hinted at...",
41
+ "url": "https://example.com/news/12345",
42
+ "source": "Reuters",
43
+ "related": "AAPL",
44
+ "datetime": "2026-05-15T14:30:00.000Z"
45
+ }
46
+ ```
47
+
48
+ ---
49
+
50
+ ### `getCompanyNews(symbol, daysBack)`
51
+
52
+ **Archivo:** `src/finnhub/finnhub.client.js`
53
+
54
+ Obtiene noticias de una empresa especifica en un rango de fechas.
55
+
56
+ **Parametros**
57
+
58
+ | Param | Tipo | Default | Descripcion |
59
+ |---|---|---|---|
60
+ | `symbol` | string | β€” | Simbolo bursatil (ej. `AAPL`, `TSLA`, `BTC`) |
61
+ | `daysBack` | int | `7` | Dias hacia atras desde hoy |
62
+
63
+ **Retorno:** `Promise<Object[]>` β€” noticias normalizadas de la empresa.
64
+
65
+ ---
66
+
67
+ ### `fetchFinancialNews(daysBack)`
68
+
69
+ **Archivo:** `src/finnhub/finnhub.service.js`
70
+
71
+ Agrega noticias de multiples categorias (`general`, `forex`, `crypto`) para el pipeline de IA.
72
+
73
+ **Parametros**
74
+
75
+ | Param | Tipo | Default | Descripcion |
76
+ |---|---|---|---|
77
+ | `daysBack` | int | `7` | No aplica directamente; se usa el rango por defecto de Finnhub |
78
+
79
+ **Retorno:** `Promise<Object[]>` β€” hasta 90 noticias agregadas (50 general + 20 forex + 20 crypto).
80
+
81
+ ---
82
+
83
+ ### `filterNewsByRelevance(news, question)`
84
+
85
+ **Archivo:** `src/finnhub/finnhub.service.js`
86
+
87
+ Filtra noticias por keywords extraidas de la pregunta del mercado. Elimina stop words en ingles y espanol; matching case-insensitive.
88
+
89
+ **Parametros**
90
+
91
+ | Param | Tipo | Descripcion |
92
+ |---|---|---|
93
+ | `news` | Object[] | Array de noticias normalizadas |
94
+ | `question` | string | Pregta del mercado (ej. "Will Bitcoin reach $100k?") |
95
+
96
+ **Retorno:** `Object[]` β€” noticias que contienen al menos una keyword en `headline` o `summary`.
97
+
98
+ ---
99
+
100
+ ### `fetchHeadlinesForPipeline({ symbols, daysBack, limitPerSymbol })`
101
+
102
+ **Archivo:** `src/finnhub/finnhub.service.js`
103
+
104
+ Obtiene noticias de multiples simbolos en paralelo, truncando a maximo 5 simbolos para respetar el rate limit.
105
+
106
+ **Parametros**
107
+
108
+ | Param | Tipo | Default | Descripcion |
109
+ |---|---|---|---|
110
+ | `symbols` | string[] | `[]` | Lista de simbolos bursatiles |
111
+ | `daysBack` | int | `7` | Dias hacia atras |
112
+ | `limitPerSymbol` | int | `20` | Maximo de noticias por simbolo |
113
+
114
+ **Retorno:** `Promise<Object[]>` β€” noticias agregadas con campo `symbol` anadido.
115
+
116
+ **Limitacion:** si `symbols.length > 5`, se trunca a 5 y se emite warning en logs.
117
+
118
+ ---
119
+
120
+ ## Pipeline de noticias dentro de la generacion de senales
121
+
122
+ ```
123
+ scheduler.js (cada 5 min)
124
+ ↓
125
+ aiPipeline.run(market)
126
+ ↓ Paso 1: Obtener noticias
127
+ fetchFinancialNews() β†’ general + forex + crypto
128
+ ↓ Paso 2: Filtrar por relevancia
129
+ filterNewsByRelevance(news, market.question)
130
+ ↓ Paso 3: Filtrar por sentimiento (FinBERT)
131
+ filterWithFinBERT(headlines)
132
+ ↓ Paso 4: Generar senal (Qwen3-8B / OpenRouter / rule-based)
133
+ ```
134
+
135
+ **Nota:** si Finnhub no esta configurado o falla, los pasos 1-2 devuelven array vacio y el pipeline continua con rule-based usando el precio del mercado.
136
+
137
+ ---
138
+
139
+ ## Rate limiter y restricciones
140
+
141
+ | Restriccion | Implementacion |
142
+ |---|---|
143
+ | **Free tier: 60 llamadas/min** | Rate limiter en memoria en `finnhub.client.js`. Si se excede, devuelve `[]`. |
144
+ | **Sin API key** | `ensureApiKey()` devuelve `false`; todas las funciones retornan `[]` sin lanzar error. |
145
+ | **Sin endpoints REST** | El modulo es puramente interno; no se expone via HTTP. |
146
+ | **Limite de 5 simbolos** | `fetchHeadlinesForPipeline` trunca `symbols` a 5 para no saturar el rate limit. |
147
+ | **Errores de red** | Capturados internamente; se loguean como `warn` y se retorna `[]`. |
148
+
149
+ **Calculo de uso tipico:**
150
+ - `fetchFinancialNews()` = 3 llamadas (general + forex + crypto)
151
+ - Top 20 mercados cada 5 min = 20 x 3 = 60 llamadas/5min (en el limite exacto del free tier)
152
+
153
+ ---
154
+
155
+ ## Ejemplo de uso interno
156
+
157
+ ```js
158
+ // Desde aiPipeline.js
159
+ import { fetchFinancialNews, filterNewsByRelevance } from '../finnhub/finnhub.service.js';
160
+
161
+ async function run(market) {
162
+ // 1. Obtener noticias
163
+ const allNews = await fetchFinancialNews(30);
164
+
165
+ // 2. Filtrar por relevancia respecto al mercado
166
+ const relevant = filterNewsByRelevance(allNews, market.question);
167
+
168
+ // 3. Continuar con FinBERT + Qwen3-8B...
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Logs esperados
175
+
176
+ ```
177
+ # Sin API key configurada
178
+ [DEBUG] Finnhub API key not configured, returning empty array
179
+
180
+ # Rate limit excedido
181
+ [WARN] Finnhub rate limit exceeded (60 calls/min), returning empty array
182
+
183
+ # Pipeline continua sin noticias
184
+ [WARN] news fetch failed, continuing without news
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Archivos del modulo
190
+
191
+ ```
192
+ backend/src/finnhub/
193
+ β”œβ”€β”€ finnhub.client.js # Cliente HTTP + rate limiter
194
+ └── finnhub.service.js # Logica de negocio y filtrado
195
+ ```
196
+
197
+ ---
198
+
199
+ *Ultima actualizacion: mayo 2026*
backend/src/alerts/alerts.service.js CHANGED
@@ -22,7 +22,7 @@
22
  import { z } from 'zod';
23
  import { alertsRepository } from './alerts.repository.js';
24
  import { watchlistRepository } from '../watchlist/watchlist.repository.js';
25
- import { sendMessage } from './telegram.client.js';
26
  import { emitPriceAlert } from '../socket/broadcaster.js';
27
  import { logger } from '../utils/logger.js';
28
 
@@ -49,10 +49,7 @@ export const alertsService = {
49
  const recent = await alertsRepository.findRecent(user.id, market.id, 'price_threshold', DEDUP_WINDOW_MS);
50
  if (recent) continue;
51
 
52
- const pct = (market.yesPrice * 100).toFixed(1);
53
- const threshold = (alertThreshold * 100).toFixed(1);
54
- const message =
55
- `<b>Price Alert</b>\n${market.question}\nYES: ${pct}% β‰₯ threshold ${threshold}%`;
56
 
57
  await alertsRepository.create({ userId: user.id, marketId: market.id, type: 'price_threshold', message });
58
  await sendMessage(user.telegramChatId, message);
 
22
  import { z } from 'zod';
23
  import { alertsRepository } from './alerts.repository.js';
24
  import { watchlistRepository } from '../watchlist/watchlist.repository.js';
25
+ import { sendMessage, formatPriceAlert } from './telegram.client.js';
26
  import { emitPriceAlert } from '../socket/broadcaster.js';
27
  import { logger } from '../utils/logger.js';
28
 
 
49
  const recent = await alertsRepository.findRecent(user.id, market.id, 'price_threshold', DEDUP_WINDOW_MS);
50
  if (recent) continue;
51
 
52
+ const message = formatPriceAlert(market.question, market.yesPrice, alertThreshold);
 
 
 
53
 
54
  await alertsRepository.create({ userId: user.id, marketId: market.id, type: 'price_threshold', message });
55
  await sendMessage(user.telegramChatId, message);
backend/src/alerts/telegram.client.js CHANGED
@@ -1,32 +1,59 @@
1
  /**
2
- * Servicio de envio de alertas por Telegram Bot API.
3
  *
4
  * Responsabilidades:
5
  * - sendMessage(chatId, text) β†’ POST a api.telegram.org/bot<TOKEN>/sendMessage.
 
 
 
6
  * - Parse_mode: HTML (permite formato basico: <b>, <i>).
7
- * - Si falta TELEGRAM_BOT_TOKEN o chatId, la funcion retorna silenciosamente.
8
  * - Errores de red se capturan y loguean como warn (no bloquean el flujo).
 
9
  *
10
  * Consumido por:
11
  * - alerts.service.js β†’ processAll() cuando se dispara una alerta.
12
  *
13
  * Seguridad:
14
  * - El token se lee de variables de entorno (nunca hardcodeado).
15
- * - Se recomienda escapar entidades HTML si el texto incluye input de usuario.
16
  */
17
 
18
  import { httpPost } from '../utils/httpClient.js';
19
  import { config } from '../config.js';
20
  import { logger } from '../utils/logger.js';
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  export async function sendMessage(chatId, text) {
23
  if (!config.TELEGRAM_BOT_TOKEN || !chatId) return;
24
  try {
25
- await httpPost(
26
  `https://api.telegram.org/bot${config.TELEGRAM_BOT_TOKEN}/sendMessage`,
27
  { chat_id: chatId, text, parse_mode: 'HTML' },
28
  { retries: 1, timeout: 8_000 },
29
  );
 
 
 
 
 
 
 
30
  } catch (err) {
31
  logger.warn({ err: err.message, chatId }, 'telegram send failed');
32
  }
 
1
  /**
2
+ * Cliente de integracion con Telegram Bot API.
3
  *
4
  * Responsabilidades:
5
  * - sendMessage(chatId, text) β†’ POST a api.telegram.org/bot<TOKEN>/sendMessage.
6
+ * - formatPriceAlert(question, yesPrice, threshold) β†’ construye mensaje HTML
7
+ * escapando entidades del input de usuario.
8
+ * - escapeHtml(text) β†’ escapa caracteres HTML sensibles (&, <, >, ").
9
  * - Parse_mode: HTML (permite formato basico: <b>, <i>).
10
+ * - Si falta TELEGRAM_BOT_TOKEN o chatId, sendMessage retorna silenciosamente.
11
  * - Errores de red se capturan y loguean como warn (no bloquean el flujo).
12
+ * - Respuestas de Telegram con ok: false se loguean como warn y no lanzan excepcion.
13
  *
14
  * Consumido por:
15
  * - alerts.service.js β†’ processAll() cuando se dispara una alerta.
16
  *
17
  * Seguridad:
18
  * - El token se lee de variables de entorno (nunca hardcodeado).
19
+ * - Todo texto dinamico se escapa via escapeHtml antes de inyectarse en HTML.
20
  */
21
 
22
  import { httpPost } from '../utils/httpClient.js';
23
  import { config } from '../config.js';
24
  import { logger } from '../utils/logger.js';
25
 
26
+ export function escapeHtml(text) {
27
+ if (typeof text !== 'string') return String(text ?? '');
28
+ return text
29
+ .replace(/&/g, '&amp;')
30
+ .replace(/</g, '&lt;')
31
+ .replace(/>/g, '&gt;')
32
+ .replace(/"/g, '&quot;');
33
+ }
34
+
35
+ export function formatPriceAlert(question, yesPrice, threshold) {
36
+ const safeQuestion = escapeHtml(question);
37
+ const pct = (yesPrice * 100).toFixed(1);
38
+ const thr = (threshold * 100).toFixed(1);
39
+ return `<b>Price Alert</b>\n${safeQuestion}\nYES: ${pct}% β‰₯ threshold ${thr}%`;
40
+ }
41
+
42
  export async function sendMessage(chatId, text) {
43
  if (!config.TELEGRAM_BOT_TOKEN || !chatId) return;
44
  try {
45
+ const result = await httpPost(
46
  `https://api.telegram.org/bot${config.TELEGRAM_BOT_TOKEN}/sendMessage`,
47
  { chat_id: chatId, text, parse_mode: 'HTML' },
48
  { retries: 1, timeout: 8_000 },
49
  );
50
+ if (result && result.ok === false) {
51
+ logger.warn(
52
+ { chatId, description: result.description, errorCode: result.error_code },
53
+ 'telegram API returned ok=false',
54
+ );
55
+ return;
56
+ }
57
  } catch (err) {
58
  logger.warn({ err: err.message, chatId }, 'telegram send failed');
59
  }