Spaces:
Sleeping
Sleeping
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
|
| 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 |
-
*
|
| 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,
|
| 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 |
-
* -
|
| 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, '&')
|
| 30 |
+
.replace(/</g, '<')
|
| 31 |
+
.replace(/>/g, '>')
|
| 32 |
+
.replace(/"/g, '"');
|
| 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 |
}
|