Jose Salazar commited on
Commit
94ad3aa
Β·
1 Parent(s): 3de0a27

Integracion de pipeline de IA y pruebas de modelos en hf spaces, documentacion de modulos

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .env.example +18 -6
  2. README.md +170 -42
  3. backend/package.json +1 -0
  4. backend/src/alerts/alerts.controller.js +10 -0
  5. backend/src/alerts/alerts.repository.js +14 -0
  6. backend/src/alerts/alerts.routes.js +9 -0
  7. backend/src/alerts/alerts.service.js +21 -0
  8. backend/src/alerts/telegram.client.js +17 -0
  9. backend/src/app.js +23 -0
  10. backend/src/auth/auth.controller.js +13 -0
  11. backend/src/auth/auth.routes.js +16 -0
  12. backend/src/auth/auth.service.js +17 -0
  13. backend/src/auth/auth.validators.js +11 -0
  14. backend/src/auth/jwt.js +16 -0
  15. backend/src/config.js +21 -0
  16. backend/src/index.js +17 -0
  17. backend/src/markets/markets.controller.js +15 -0
  18. backend/src/markets/markets.repository.js +13 -0
  19. backend/src/markets/markets.routes.js +10 -0
  20. backend/src/markets/markets.service.js +15 -0
  21. backend/src/markets/markets.validators.js +11 -0
  22. backend/src/markets/polymarket.client.js +16 -0
  23. backend/src/middlewares/errorHandler.js +12 -0
  24. backend/src/middlewares/notFound.js +10 -0
  25. backend/src/middlewares/rateLimitLogin.js +12 -0
  26. backend/src/middlewares/requireAuth.js +16 -0
  27. backend/src/middlewares/validate.js +16 -0
  28. backend/src/positions/kelly.js +16 -3
  29. backend/src/positions/positions.controller.js +14 -0
  30. backend/src/positions/positions.repository.js +13 -0
  31. backend/src/positions/positions.routes.js +11 -0
  32. backend/src/positions/positions.service.js +20 -0
  33. backend/src/positions/positions.validators.js +12 -0
  34. backend/src/scheduler.js +13 -0
  35. backend/src/signals/aiPipeline.js +128 -10
  36. backend/src/signals/finnhub.client.js +16 -0
  37. backend/src/signals/signals.controller.js +14 -0
  38. backend/src/signals/signals.repository.js +13 -0
  39. backend/src/signals/signals.routes.js +9 -0
  40. backend/src/signals/signals.service.js +18 -0
  41. backend/src/socket/broadcaster.js +15 -0
  42. backend/src/utils/apiResponse.js +14 -0
  43. backend/src/utils/httpClient.js +15 -0
  44. backend/src/utils/logger.js +11 -0
  45. backend/src/utils/prisma.js +13 -0
  46. backend/src/watchlist/watchlist.controller.js +14 -0
  47. backend/src/watchlist/watchlist.repository.js +15 -0
  48. backend/src/watchlist/watchlist.routes.js +11 -0
  49. backend/src/watchlist/watchlist.service.js +15 -0
  50. backend/src/watchlist/watchlist.validators.js +11 -0
.env.example CHANGED
@@ -4,10 +4,22 @@
4
  # Nunca commitear el archivo .env real (debe estar en .gitignore).
5
  # Ubicacion recomendada: en la raiz del proyecto (al lado de docker-compose.yml).
6
 
7
- HF_TOKEN= # HuggingFace Pro API key (Inference API)
8
- OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
9
- FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
10
- TELEGRAM_BOT_TOKEN= # Bot de alertas (@BotFather)
11
- DATABASE_URL=file:./backend/prisma/polysignal.db # SQLite local (relativo a backend/)
12
- PORT=7860 # HuggingFace Spaces requiere 7860
 
 
 
 
 
 
 
 
 
 
 
 
13
  NODE_ENV=production
 
4
  # Nunca commitear el archivo .env real (debe estar en .gitignore).
5
  # Ubicacion recomendada: en la raiz del proyecto (al lado de docker-compose.yml).
6
 
7
+ # HuggingFace
8
+ HF_TOKEN= # HuggingFace Pro API key (Inference API)
9
+ HF_SPACE_MODERNFINBERT_URL=blackmistcode/polysignal-modernfinbert
10
+ HF_SPACE_QWEN_URL=blackmistcode/polysignal-qwen3-8b
11
+
12
+ # Fallbacks y datos
13
+ OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
14
+ FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
15
+
16
+ # Alertas
17
+ TELEGRAM_BOT_TOKEN= # Bot de alertas (@BotFather)
18
+
19
+ # Base de datos y auth
20
+ DATABASE_URL=file:./backend/prisma/polysignal.db
21
+ JWT_SECRET=supersecreto-demasiado-largo-para-la-hackathon-2026
22
+
23
+ # Servidor
24
+ PORT=7860 # HuggingFace Spaces requiere 7860
25
  NODE_ENV=production
README.md CHANGED
@@ -8,55 +8,127 @@ PolySignal analiza mercados de Polymarket cruzando noticias de Finnhub con model
8
 
9
  **No ejecuta ordenes reales.** Es una herramienta de analisis e inteligencia.
10
 
11
- **Idioma:** Espanol.
12
  **Moneda base:** Euro (€).
13
 
14
  ## Stack
15
 
16
  - **Backend:** Node.js 26 + Express.js 5 + Socket.io + node-cron
17
- - **ORM:** Prisma 7 + SQLite
18
- - **Frontend:** Vanilla JS + Vite 8 + Leaflet.js + Chart.js + Socket.io client
19
- - **IA:** HuggingFace Inference API (ModernFinBERT, Qwen3-8B) + OpenRouter fallback
20
  - **Datos:** Polymarket Gamma API + Finnhub REST
21
  - **Deploy:** HuggingFace Spaces (Docker, puerto 7860)
22
 
23
  ## Estado del proyecto
24
 
25
- > **Nota importante:** El backend actualmente contiene solo stubs/documentacion JSDoc. El frontend funciona de forma independiente con **datos mock** realistas para la demo. Puedes ejecutar y visualizar todo el dashboard sin necesidad de configurar claves de API ni base de datos.
 
 
 
 
 
 
 
 
26
 
27
  ## Estructura
28
 
29
  ```
30
  polysignal/
31
- β”œβ”€β”€ backend/ # API REST + Servicios + Scheduler (stubs)
32
  β”‚ β”œβ”€β”€ package.json
33
  β”‚ β”œβ”€β”€ prisma/
34
- β”‚ β”‚ └── schema.prisma # Schema SQLite (Prisma)
 
 
35
  β”‚ └── src/
36
- β”‚ β”œβ”€β”€ index.js # Entry point Express
37
- β”‚ β”œβ”€β”€ config.js # Variables de entorno
38
- β”‚ β”œβ”€β”€ scheduler.js # Jobs periodicos (cron)
39
- β”‚ β”œβ”€β”€ routes/ # Endpoints REST (/api/v1)
40
- β”‚ β”œβ”€β”€ services/ # Logica de negocio + integraciones
41
- β”‚ └── socket/ # Eventos Socket.io
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  β”‚
43
- β”œβ”€β”€ frontend/ # SPA Vanilla JS con Vite
44
- β”‚ β”œβ”€β”€ index.html # Punto de entrada HTML
45
- β”‚ β”œβ”€β”€ package.json # Dependencias del frontend
46
- β”‚ β”œβ”€β”€ vite.config.js # Configuracion de Vite (proxy + build)
47
  β”‚ └── src/
48
- β”‚ β”œβ”€β”€ main.js # Entry point de Vite
49
- β”‚ β”œβ”€β”€ app.js # Logica principal SPA + Socket.io
50
- β”‚ β”œβ”€β”€ api.js # Cliente REST
51
- β”‚ β”œβ”€β”€ charts.js # Chart.js (historial + sparklines)
52
- β”‚ β”œβ”€β”€ map.js # Leaflet (mapa mundial interactivo)
53
- β”‚ β”œβ”€β”€ simulator.js # Simulador de posiciones virtuales
54
- β”‚ └── style.css # Estilos dark terminal / fintech
 
 
 
 
 
 
 
 
 
 
 
55
  β”‚
56
- β”œβ”€β”€ package.json # Root con workspaces + scripts conjuntos
57
- β”œβ”€β”€ docker-compose.yml # Orquestacion local
58
- β”œβ”€β”€ Dockerfile # Build para HuggingFace Spaces
59
- β”œβ”€β”€ .env.example # Variables de entorno de ejemplo
 
60
  └── README.md
61
  ```
62
 
@@ -71,13 +143,11 @@ polysignal/
71
  # 1. Instalar dependencias (root + todos los workspaces)
72
  npm install
73
 
74
- # 2. (Opcional) Configurar variables de entorno para backend
75
- # Si solo vas a probar el frontend, este paso NO es necesario.
76
  cp .env.example .env
77
- # Editar .env con tus claves
78
 
79
- # 3. (Opcional) Generar base de datos y cliente Prisma
80
- # Solo necesario si vas a desarrollar el backend.
81
  npm run db:migrate
82
  npm run db:generate
83
 
@@ -98,9 +168,46 @@ npm run dev
98
 
99
  El frontend consume datos mock localmente cuando el backend no responde, por lo que el dashboard es totalmente funcional para la demo sin configuracion adicional.
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  ## Arquitectura del Frontend
102
 
103
- El frontend es una SPA (Single Page Application) construida con **Vite 8** como bundler y dev server. Las librerias (Chart.js, Leaflet, Socket.io client) se gestionan como dependencias npm en lugar de CDN, lo que permite tree-shaking y un build optimizado para produccion.
104
 
105
  ### Caracteristicas visuales
106
 
@@ -130,7 +237,7 @@ npm run dev
130
  # Levantar solo el frontend (Vite con hot reload)
131
  npm run dev:frontend
132
 
133
- # Levantar backend y frontend simultaneamente en una sola terminal
134
  npm run dev:all
135
 
136
  # Build de produccion del frontend (genera frontend/dist/)
@@ -138,16 +245,13 @@ npm run build:frontend
138
 
139
  # Preview del build de produccion
140
  npm run preview:frontend
141
- ```
142
-
143
- ### Build de produccion
144
 
145
- ```bash
146
- npm run build:frontend
 
 
147
  ```
148
 
149
- Esto genera los assets optimizados en `frontend/dist/`. En produccion (HuggingFace Spaces), el backend Express sirve directamente esa carpeta como archivos estaticos.
150
-
151
  ## Deploy en HuggingFace Spaces
152
 
153
  1. Crear Space tipo "Docker"
@@ -166,6 +270,30 @@ docker build -t polysignal .
166
  docker run -p 7860:7860 --env-file .env polysignal
167
  ```
168
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  ## Equipo
170
 
171
  Hackathon CIFO Barcelona La Violeta β€” 13-18 mayo 2026
 
8
 
9
  **No ejecuta ordenes reales.** Es una herramienta de analisis e inteligencia.
10
 
11
+ **Idioma:** Espanol.
12
  **Moneda base:** Euro (€).
13
 
14
  ## Stack
15
 
16
  - **Backend:** Node.js 26 + Express.js 5 + Socket.io + node-cron
17
+ - **ORM:** Prisma 6 + SQLite
18
+ - **Frontend:** Vanilla JS + Vite 7 + Leaflet.js + Chart.js + Socket.io client
19
+ - **IA:** HuggingFace Spaces (ModernFinBERT + Qwen3-8B) + OpenRouter fallback
20
  - **Datos:** Polymarket Gamma API + Finnhub REST
21
  - **Deploy:** HuggingFace Spaces (Docker, puerto 7860)
22
 
23
  ## Estado del proyecto
24
 
25
+ El backend es **totalmente funcional**:
26
+
27
+ - API REST completa con autenticacion JWT.
28
+ - Pipeline de IA con cadena de fallback (Spaces HF β†’ API HF β†’ OpenRouter β†’ rule-based).
29
+ - Scheduler de tareas periodicas (sync mercados, generacion de senales, P&L, alertas).
30
+ - WebSocket en tiempo real para precios y senales.
31
+ - Base de datos SQLite con Prisma ORM.
32
+
33
+ El frontend **consume datos reales del backend** y tiene **fallback a datos mock** cuando el backend no responde (modo demo sin configuracion).
34
 
35
  ## Estructura
36
 
37
  ```
38
  polysignal/
39
+ β”œβ”€β”€ backend/ # API REST + Servicios + Scheduler
40
  β”‚ β”œβ”€β”€ package.json
41
  β”‚ β”œβ”€β”€ prisma/
42
+ β”‚ β”‚ β”œβ”€β”€ schema.prisma # Schema SQLite (User, Market, AISignal, Position, Watchlist, Alert)
43
+ β”‚ β”‚ β”œβ”€β”€ migrations/ # Migraciones de Prisma
44
+ β”‚ β”‚ └── seed.js # Usuarios demo (admin + user)
45
  β”‚ └── src/
46
+ β”‚ β”œβ”€β”€ index.js # Entry point: HTTP server + Socket.io + scheduler
47
+ β”‚ β”œβ”€β”€ app.js # Express: middlewares + rutas + manejo de errores
48
+ β”‚ β”œβ”€β”€ config.js # Variables de entorno validadas con Zod
49
+ β”‚ β”œβ”€β”€ scheduler.js # Jobs periodicos (cron): sync, senales IA, PnL, alertas
50
+ β”‚ β”œβ”€β”€ auth/ # Autenticacion JWT + bcrypt
51
+ β”‚ β”‚ β”œβ”€β”€ auth.controller.js
52
+ β”‚ β”‚ β”œβ”€β”€ auth.service.js
53
+ β”‚ β”‚ β”œβ”€β”€ auth.routes.js
54
+ β”‚ β”‚ β”œβ”€β”€ auth.validators.js
55
+ β”‚ β”‚ └── jwt.js
56
+ β”‚ β”œβ”€β”€ markets/ # Mercados de Polymarket
57
+ β”‚ β”‚ β”œβ”€β”€ markets.controller.js
58
+ β”‚ β”‚ β”œβ”€β”€ markets.service.js
59
+ β”‚ β”‚ β”œβ”€β”€ markets.routes.js
60
+ β”‚ β”‚ β”œβ”€β”€ markets.validators.js
61
+ β”‚ β”‚ β”œβ”€β”€ markets.repository.js
62
+ β”‚ β”‚ └── polymarket.client.js
63
+ β”‚ β”œβ”€β”€ signals/ # Pipeline de IA (ModernFinBERT + Qwen3-8B)
64
+ β”‚ β”‚ β”œβ”€β”€ signals.controller.js
65
+ β”‚ β”‚ β”œβ”€β”€ signals.service.js
66
+ β”‚ β”‚ β”œβ”€β”€ signals.routes.js
67
+ β”‚ β”‚ β”œβ”€β”€ signals.repository.js
68
+ β”‚ β”‚ β”œβ”€β”€ aiPipeline.js # Pipeline IA con fallback chain
69
+ β”‚ β”‚ └── finnhub.client.js # Noticias financieras
70
+ β”‚ β”œβ”€β”€ positions/ # Simulador de posiciones virtuales
71
+ β”‚ β”‚ β”œβ”€β”€ positions.controller.js
72
+ β”‚ β”‚ β”œβ”€β”€ positions.service.js
73
+ β”‚ β”‚ β”œβ”€β”€ positions.routes.js
74
+ β”‚ β”‚ β”œβ”€β”€ positions.validators.js
75
+ β”‚ β”‚ β”œβ”€β”€ positions.repository.js
76
+ β”‚ β”‚ └── kelly.js # Criterio de Kelly (sizing)
77
+ β”‚ β”œβ”€β”€ watchlist/ # Lista de seguimiento
78
+ β”‚ β”‚ β”œβ”€β”€ watchlist.controller.js
79
+ β”‚ β”‚ β”œβ”€β”€ watchlist.service.js
80
+ β”‚ β”‚ β”œβ”€β”€ watchlist.routes.js
81
+ β”‚ β”‚ β”œβ”€β”€ watchlist.validators.js
82
+ β”‚ β”‚ └── watchlist.repository.js
83
+ β”‚ β”œβ”€β”€ alerts/ # Alertas por Telegram
84
+ β”‚ β”‚ β”œβ”€β”€ alerts.controller.js
85
+ β”‚ β”‚ β”œβ”€β”€ alerts.service.js
86
+ β”‚ β”‚ β”œβ”€β”€ alerts.routes.js
87
+ β”‚ β”‚ β”œβ”€β”€ alerts.repository.js
88
+ β”‚ β”‚ └── telegram.client.js
89
+ β”‚ β”œβ”€β”€ middlewares/ # Middlewares reutilizables
90
+ β”‚ β”‚ β”œβ”€β”€ validate.js # Validacion Zod generica
91
+ β”‚ β”‚ β”œβ”€β”€ requireAuth.js # Autenticacion JWT
92
+ β”‚ β”‚ β”œβ”€β”€ rateLimitLogin.js # Rate limit login
93
+ β”‚ β”‚ β”œβ”€β”€ errorHandler.js # Manejo centralizado de errores
94
+ β”‚ β”‚ └── notFound.js # 404
95
+ β”‚ β”œβ”€β”€ utils/ # Utilidades compartidas
96
+ β”‚ β”‚ β”œβ”€β”€ apiResponse.js # Helpers de respuesta HTTP
97
+ β”‚ β”‚ β”œβ”€β”€ httpClient.js # Cliente HTTP con retry + timeout
98
+ β”‚ β”‚ β”œβ”€β”€ logger.js # Pino (logs estructurados)
99
+ β”‚ β”‚ └── prisma.js # Singleton PrismaClient
100
+ β”‚ └── socket/
101
+ β”‚ └── broadcaster.js # Emisor de eventos Socket.io
102
  β”‚
103
+ β”œβ”€β”€ frontend/ # SPA Vanilla JS con Vite
104
+ β”‚ β”œβ”€β”€ index.html # Punto de entrada HTML
105
+ β”‚ β”œβ”€β”€ package.json
106
+ β”‚ β”œβ”€β”€ vite.config.js # Proxy a backend + build config
107
  β”‚ └── src/
108
+ β”‚ β”œβ”€β”€ main.js # Entry point de Vite
109
+ β”‚ β”œβ”€β”€ app.js # Logica principal SPA + Socket.io
110
+ β”‚ β”œβ”€β”€ api.js # Cliente REST del backend
111
+ β”‚ β”œβ”€β”€ charts.js # Chart.js (historial + sparklines)
112
+ β”‚ β”œβ”€β”€ map.js # Leaflet (mapa mundial interactivo)
113
+ β”‚ β”œβ”€β”€ simulator.js # Simulador de posiciones virtuales
114
+ β”‚ └── style.css # Estilos dark terminal / fintech
115
+ β”‚
116
+ β”œβ”€β”€ spaces/ # HuggingFace Spaces (ZeroGPU)
117
+ β”‚ β”œβ”€β”€ modernfinbert/ # Space de ModernFinBERT
118
+ β”‚ β”‚ β”œβ”€β”€ app.py
119
+ β”‚ β”‚ β”œβ”€β”€ requirements.txt
120
+ β”‚ β”‚ └── README.md
121
+ β”‚ └── qwen3-8b/ # Space de Qwen3-8B
122
+ β”‚ β”œβ”€β”€ app.py
123
+ β”‚ β”œβ”€β”€ Dockerfile
124
+ β”‚ β”œβ”€β”€ requirements.txt
125
+ β”‚ └── README.md
126
  β”‚
127
+ β”œβ”€β”€ package.json # Root con workspaces + scripts conjuntos
128
+ β”œβ”€β”€ docker-compose.yml # Orquestacion local
129
+ β”œβ”€β”€ Dockerfile # Build para HuggingFace Spaces
130
+ β”œβ”€β”€ .env.example # Variables de entorno de ejemplo
131
+ β”œβ”€β”€ SECURITY_HEALTHCHECK.md # Auditoria de seguridad y arquitectura
132
  └── README.md
133
  ```
134
 
 
143
  # 1. Instalar dependencias (root + todos los workspaces)
144
  npm install
145
 
146
+ # 2. Configurar variables de entorno
 
147
  cp .env.example .env
148
+ # Editar .env con tus claves (HF_TOKEN, HF_SPACE_*, OPENROUTER_API_KEY, etc.)
149
 
150
+ # 3. Generar base de datos y cliente Prisma
 
151
  npm run db:migrate
152
  npm run db:generate
153
 
 
168
 
169
  El frontend consume datos mock localmente cuando el backend no responde, por lo que el dashboard es totalmente funcional para la demo sin configuracion adicional.
170
 
171
+ ## Arquitectura del Backend
172
+
173
+ El backend sigue una arquitectura **Layered (Controller β†’ Service β†’ Repository)**:
174
+
175
+ | Capa | Responsabilidad | Ejemplo |
176
+ |------|----------------|---------|
177
+ | **Controller** | Recibir HTTP request, delegar a Service, responder | `markets.controller.js` |
178
+ | **Service** | Logica de negocio, validaciones, coordinacion | `markets.service.js` |
179
+ | **Repository** | Acceso a datos via Prisma ORM | `markets.repository.js` |
180
+ | **Client** | Integracion con APIs externas | `polymarket.client.js`, `finnhub.client.js` |
181
+ | **Middleware** | Cross-cutting concerns (auth, validacion, rate limiting) | `requireAuth.js`, `validate.js` |
182
+
183
+ ### Pipeline de IA
184
+
185
+ ```
186
+ Noticias (Finnhub)
187
+ ↓
188
+ Filtrado (ModernFinBERT Space / API directa)
189
+ ↓ (descarta neutrales, score < 0.65)
190
+ Generacion de senal (Qwen3-8B Space / API directa)
191
+ ↓
192
+ Fallback: OpenRouter (deepseek-chat)
193
+ ↓
194
+ Fallback: Rule-based (precio del mercado)
195
+ ↓
196
+ Persistencia (SQLite) + Emision Socket.io
197
+ ```
198
+
199
+ ### Scheduler (node-cron)
200
+
201
+ | Job | Frecuencia | Descripcion |
202
+ |-----|-----------|-------------|
203
+ | syncMarkets | Cada 30s | Sincroniza precios desde Polymarket Gamma API |
204
+ | generateSignals | Cada 5 min | Genera senales IA para top 20 mercados activos |
205
+ | updatePositionsPnL | Cada 30s | Recalcula P&L de posiciones abiertas |
206
+ | processAlerts | Cada 60s | Revisa watchlist y envia alertas Telegram |
207
+
208
  ## Arquitectura del Frontend
209
 
210
+ El frontend es una SPA construida con **Vite 7** como bundler y dev server.
211
 
212
  ### Caracteristicas visuales
213
 
 
237
  # Levantar solo el frontend (Vite con hot reload)
238
  npm run dev:frontend
239
 
240
+ # Levantar backend y frontend simultaneamente
241
  npm run dev:all
242
 
243
  # Build de produccion del frontend (genera frontend/dist/)
 
245
 
246
  # Preview del build de produccion
247
  npm run preview:frontend
 
 
 
248
 
249
+ # Base de datos
250
+ npm run db:migrate # Crear/actualizar migraciones
251
+ npm run db:generate # Generar cliente Prisma
252
+ npm run db:studio # Explorar BD con Prisma Studio
253
  ```
254
 
 
 
255
  ## Deploy en HuggingFace Spaces
256
 
257
  1. Crear Space tipo "Docker"
 
270
  docker run -p 7860:7860 --env-file .env polysignal
271
  ```
272
 
273
+ ## Variables de entorno
274
+
275
+ ```env
276
+ # HuggingFace
277
+ HF_TOKEN= # API key de HuggingFace (Inference API)
278
+ HF_SPACE_MODERNFINBERT_URL= # URL del Space (ej: usuario/modernfinbert)
279
+ HF_SPACE_QWEN_URL= # URL del Space (ej: usuario/qwen3-8b)
280
+
281
+ # Fallbacks y datos
282
+ OPENROUTER_API_KEY= # Fallback LLM si HF esta saturado
283
+ FINNHUB_API_KEY= # Noticias financieras (finnhub.io)
284
+
285
+ # Alertas
286
+ TELEGRAM_BOT_TOKEN= # Bot de alertas (@BotFather)
287
+
288
+ # Base de datos y auth
289
+ DATABASE_URL=file:./backend/prisma/polysignal.db
290
+ JWT_SECRET=minimo-32-caracteres # Secreto para firmar JWT
291
+
292
+ # Servidor
293
+ PORT=7860 # Puerto requerido por HuggingFace Spaces
294
+ NODE_ENV=production # development | production
295
+ ```
296
+
297
  ## Equipo
298
 
299
  Hackathon CIFO Barcelona La Violeta β€” 13-18 mayo 2026
backend/package.json CHANGED
@@ -17,6 +17,7 @@
17
  "db:studio": "prisma studio"
18
  },
19
  "dependencies": {
 
20
  "@prisma/client": "^6.19.2",
21
  "bcryptjs": "^2.4.3",
22
  "cors": "^2.8.5",
 
17
  "db:studio": "prisma studio"
18
  },
19
  "dependencies": {
20
+ "@gradio/client": "^2.2.0",
21
  "@prisma/client": "^6.19.2",
22
  "bcryptjs": "^2.4.3",
23
  "cors": "^2.8.5",
backend/src/alerts/alerts.controller.js CHANGED
@@ -1,3 +1,13 @@
 
 
 
 
 
 
 
 
 
 
1
  import { ok } from '../utils/apiResponse.js';
2
  import { alertsService } from './alerts.service.js';
3
 
 
1
+ /**
2
+ * Controladores del modulo de alertas.
3
+ *
4
+ * Responsabilidades:
5
+ * - list(req, res) β†’ devuelve el historial de alertas del usuario autenticado.
6
+ *
7
+ * Endpoint (bajo /api/v1/alerts, protegido por requireAuth):
8
+ * GET / β†’ lista paginada de alertas.
9
+ */
10
+
11
  import { ok } from '../utils/apiResponse.js';
12
  import { alertsService } from './alerts.service.js';
13
 
backend/src/alerts/alerts.repository.js CHANGED
@@ -1,3 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { prisma } from '../utils/prisma.js';
2
 
3
  export const alertsRepository = {
 
1
+ /**
2
+ * Repositorio de acceso a datos para el modelo Alert.
3
+ *
4
+ * Responsabilidades:
5
+ * - create(data) β†’ inserta una nueva alerta.
6
+ * - findByUser(...) β†’ alertas paginadas del usuario con datos del mercado.
7
+ * - findRecent(...) β†’ busca alerta reciente en ventana de tiempo (deduplicacion).
8
+ *
9
+ * Campos:
10
+ * type (price_threshold|signal_change), message, sentAt.
11
+ *
12
+ * Todas las operaciones usan Prisma ORM.
13
+ */
14
+
15
  import { prisma } from '../utils/prisma.js';
16
 
17
  export const alertsRepository = {
backend/src/alerts/alerts.routes.js CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import { Router } from 'express';
2
  import { requireAuth } from '../middlewares/requireAuth.js';
3
  import { alertsController } from './alerts.controller.js';
 
1
+ /**
2
+ * Rutas REST del modulo de alertas.
3
+ *
4
+ * Endpoint (montado en /api/v1/alerts):
5
+ * GET / β†’ listar alertas del usuario autenticado.
6
+ *
7
+ * Requiere autenticacion JWT (router.use(requireAuth)).
8
+ */
9
+
10
  import { Router } from 'express';
11
  import { requireAuth } from '../middlewares/requireAuth.js';
12
  import { alertsController } from './alerts.controller.js';
backend/src/alerts/alerts.service.js CHANGED
@@ -1,3 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { z } from 'zod';
2
  import { alertsRepository } from './alerts.repository.js';
3
  import { watchlistRepository } from '../watchlist/watchlist.repository.js';
 
1
+ /**
2
+ * Logica de negocio del modulo de alertas.
3
+ *
4
+ * Responsabilidades:
5
+ * - list(userId, query) β†’ devuelve alertas paginadas del usuario.
6
+ * - processAll() β†’ revisa la watchlist cada 60s (scheduler):
7
+ * 1. Obtiene entradas de watchlist con alertThreshold.
8
+ * 2. Si yesPrice >= alertThreshold y no hay alerta reciente (5 min dedup):
9
+ * - Crea alerta en base de datos.
10
+ * - EnvΓ­a mensaje por Telegram (si hay bot token y chatId).
11
+ * - Emite evento 'price_alert' por Socket.io.
12
+ *
13
+ * Tipos de alerta:
14
+ * - price_threshold : umbral de precio cruzado.
15
+ * - signal_change : cambio de senal IA (reservado para futuras versiones).
16
+ *
17
+ * Consumido por:
18
+ * - alerts.controller.js (API REST).
19
+ * - scheduler.js (processAlerts cada 60s).
20
+ */
21
+
22
  import { z } from 'zod';
23
  import { alertsRepository } from './alerts.repository.js';
24
  import { watchlistRepository } from '../watchlist/watchlist.repository.js';
backend/src/alerts/telegram.client.js CHANGED
@@ -1,3 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { httpPost } from '../utils/httpClient.js';
2
  import { config } from '../config.js';
3
  import { logger } from '../utils/logger.js';
 
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';
backend/src/app.js CHANGED
@@ -1,3 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import express from 'express';
2
  import cors from 'cors';
3
  import helmet from 'helmet';
 
1
+ /**
2
+ * Aplicacion Express principal β€” configuracion de middlewares y montaje de rutas.
3
+ *
4
+ * Middlewares aplicados (en orden):
5
+ * 1. helmet() β€” headers de seguridad (X-Frame-Options, HSTS, etc.).
6
+ * 2. cors() β€” CORS con origen configurable (CORS_ORIGIN).
7
+ * 3. rateLimit() β€” 200 peticiones / 15 min por IP.
8
+ * 4. express.json() β€” parseo de JSON con limite de 1 MB.
9
+ *
10
+ * Rutas REST montadas bajo /api/v1:
11
+ * - /auth β†’ login, perfil (auth.routes.js)
12
+ * - /markets β†’ listado y detalle de mercados (markets.routes.js)
13
+ * - /markets β†’ senales IA por mercado (signals.routes.js, subruta)
14
+ * - /positions β†’ simulador de posiciones virtuales (positions.routes.js)
15
+ * - /watchlist β†’ lista de seguimiento (watchlist.routes.js)
16
+ * - /alerts β†’ historial de alertas (alerts.routes.js)
17
+ * - /health β†’ healthcheck basico
18
+ *
19
+ * Manejo de errores:
20
+ * - notFound β†’ 404 para rutas no definidas.
21
+ * - errorHandler β†’ 500 generico en produccion, detalles en desarrollo.
22
+ */
23
+
24
  import express from 'express';
25
  import cors from 'cors';
26
  import helmet from 'helmet';
backend/src/auth/auth.controller.js CHANGED
@@ -1,3 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import * as authService from './auth.service.js';
2
  import { ok } from '../utils/apiResponse.js';
3
 
 
1
+ /**
2
+ * Controladores del modulo de autenticacion.
3
+ *
4
+ * Responsabilidades:
5
+ * - login β†’ recibir credenciales, delegar validacion a auth.service.js,
6
+ * responder con { token, user }.
7
+ * - me β†’ devolver el usuario autenticado extraido del JWT (req.user).
8
+ *
9
+ * Errores:
10
+ * - 401 INVALID_CREDENTIALS β†’ email o password incorrectos (mensaje generico).
11
+ * - 401 UNAUTHORIZED β†’ token invalido o ausente (en requireAuth).
12
+ */
13
+
14
  import * as authService from './auth.service.js';
15
  import { ok } from '../utils/apiResponse.js';
16
 
backend/src/auth/auth.routes.js CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { Router } from 'express';
2
  import * as ctrl from './auth.controller.js';
3
  import { loginSchema } from './auth.validators.js';
 
1
+ /**
2
+ * Rutas REST de autenticacion.
3
+ *
4
+ * Endpoints:
5
+ * POST /api/v1/auth/login
6
+ * β†’ rateLimitLogin (5 intentos / 15 min)
7
+ * β†’ validate(loginSchema)
8
+ * β†’ authController.login
9
+ * β†’ Devuelve JWT + objeto usuario.
10
+ *
11
+ * GET /api/v1/auth/me
12
+ * β†’ requireAuth
13
+ * β†’ authController.me
14
+ * β†’ Devuelve el usuario autenticado (req.user).
15
+ */
16
+
17
  import { Router } from 'express';
18
  import * as ctrl from './auth.controller.js';
19
  import { loginSchema } from './auth.validators.js';
backend/src/auth/auth.service.js CHANGED
@@ -1,3 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import bcrypt from 'bcryptjs';
2
  import { prisma } from '../utils/prisma.js';
3
  import { HttpError } from '../utils/apiResponse.js';
 
1
+ /**
2
+ * Logica de negocio del modulo de autenticacion.
3
+ *
4
+ * Responsabilidades:
5
+ * - login({ email, password }) β†’ buscar usuario, comparar hash con bcrypt,
6
+ * verificar que este activo (isActive) y firmar JWT.
7
+ *
8
+ * Seguridad:
9
+ * - Mensaje generico en fallo ("Email or password is incorrect")
10
+ * para no revelar si el email existe.
11
+ * - Bcrypt con salt rounds configurable (BCRYPT_ROUNDS, default 10).
12
+ * - JWT firmado con HS256 y expiracion (JWT_EXPIRES_IN).
13
+ *
14
+ * Devuelve:
15
+ * { token: string, user: { id, email } }
16
+ */
17
+
18
  import bcrypt from 'bcryptjs';
19
  import { prisma } from '../utils/prisma.js';
20
  import { HttpError } from '../utils/apiResponse.js';
backend/src/auth/auth.validators.js CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import { z } from 'zod';
2
 
3
  export const loginSchema = z.object({
 
1
+ /**
2
+ * Esquemas Zod para validar inputs del modulo de autenticacion.
3
+ *
4
+ * Responsabilidades:
5
+ * - loginSchema: validar email (formato correcto, lowercase, trim)
6
+ * y password (minimo 8 caracteres).
7
+ *
8
+ * Consumido por:
9
+ * - auth.routes.js β†’ validate(loginSchema) en POST /login.
10
+ */
11
+
12
  import { z } from 'zod';
13
 
14
  export const loginSchema = z.object({
backend/src/auth/jwt.js CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import jwt from 'jsonwebtoken';
2
  import { config } from '../config.js';
3
 
 
1
+ /**
2
+ * Helpers para firmar y verificar tokens JWT.
3
+ *
4
+ * Responsabilidades:
5
+ * - signToken(payload) β†’ firma un token HS256 con expiracion configurable.
6
+ * - verifyToken(token) β†’ verifica firma, expiracion y algoritmo (solo HS256).
7
+ *
8
+ * Consumido por:
9
+ * - auth.service.js β†’ al hacer login exitoso.
10
+ * - requireAuth.js β†’ en cada peticion protegida.
11
+ *
12
+ * Configuracion:
13
+ * - JWT_SECRET : minimo 32 chars (validado en config.js).
14
+ * - JWT_EXPIRES_IN: default '1h'.
15
+ */
16
+
17
  import jwt from 'jsonwebtoken';
18
  import { config } from '../config.js';
19
 
backend/src/config.js CHANGED
@@ -1,3 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import 'dotenv/config';
2
  import { z } from 'zod';
3
 
@@ -11,6 +30,8 @@ const schema = z.object({
11
  CORS_ORIGIN: z.string().default('http://localhost:5173'),
12
  LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
13
  HF_TOKEN: z.string().optional(),
 
 
14
  OPENROUTER_API_KEY: z.string().optional(),
15
  FINNHUB_API_KEY: z.string().optional(),
16
  TELEGRAM_BOT_TOKEN: z.string().optional(),
 
1
+ /**
2
+ * Configuracion centralizada de variables de entorno.
3
+ *
4
+ * Carga los valores de process.env mediante dotenv, los valida con Zod
5
+ * y expone constantes tipadas como: PORT, DATABASE_URL, JWT_SECRET,
6
+ * HF_TOKEN, HF_SPACE_MODERNFINBERT_URL, HF_SPACE_QWEN_URL, etc.
7
+ *
8
+ * Variables clave:
9
+ * - PORT: 7860 (requerido por HuggingFace Spaces).
10
+ * - DATABASE_URL: SQLite local para desarrollo, PostgreSQL para produccion.
11
+ * - JWT_SECRET: minimo 32 caracteres, usado para firmar tokens de autenticacion.
12
+ * - HF_TOKEN / HF_SPACE_*: credenciales para los Spaces de HuggingFace (IA).
13
+ * - OPENROUTER_API_KEY: fallback LLM si los Spaces estan saturados.
14
+ * - FINNHUB_API_KEY: noticias financieras para el pipeline de senales.
15
+ * - TELEGRAM_BOT_TOKEN: bot de alertas (@BotFather).
16
+ *
17
+ * Si falla la validacion, el proceso termina con error antes de levantar el servidor.
18
+ */
19
+
20
  import 'dotenv/config';
21
  import { z } from 'zod';
22
 
 
30
  CORS_ORIGIN: z.string().default('http://localhost:5173'),
31
  LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
32
  HF_TOKEN: z.string().optional(),
33
+ HF_SPACE_MODERNFINBERT_URL: z.string().optional(),
34
+ HF_SPACE_QWEN_URL: z.string().optional(),
35
  OPENROUTER_API_KEY: z.string().optional(),
36
  FINNHUB_API_KEY: z.string().optional(),
37
  TELEGRAM_BOT_TOKEN: z.string().optional(),
backend/src/index.js CHANGED
@@ -1,3 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import http from 'node:http';
2
  import { Server as IOServer } from 'socket.io';
3
  import app from './app.js';
 
1
+ /**
2
+ * Entry point de la aplicacion PolySignal.
3
+ *
4
+ * Inicializa el servidor HTTP nativo de Node.js, monta Express sobre el,
5
+ * configura Socket.io para comunicacion en tiempo real, arranca el scheduler
6
+ * de tareas periodicas (node-cron) y gestiona el cierre limpio ante SIGTERM/SIGINT.
7
+ *
8
+ * Flujo de arranque:
9
+ * 1. Crear servidor HTTP y adjuntar Express (app.js).
10
+ * 2. Inicializar Socket.io con CORS permitido desde CORS_ORIGIN.
11
+ * 3. Conectar el broadcaster (socket/broadcaster.js) para emitir eventos.
12
+ * 4. Escuchar en el puerto configurado (default 7860 para HF Spaces).
13
+ * 5. En modo no-test, iniciar scheduler (sync mercados, senales IA, PnL, alertas).
14
+ *
15
+ * Puerto por defecto: 7860 (requerido por HuggingFace Spaces).
16
+ */
17
+
18
  import http from 'node:http';
19
  import { Server as IOServer } from 'socket.io';
20
  import app from './app.js';
backend/src/markets/markets.controller.js CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { ok } from '../utils/apiResponse.js';
2
  import { marketsService } from './markets.service.js';
3
 
 
1
+ /**
2
+ * Controladores del modulo de mercados.
3
+ *
4
+ * Responsabilidades:
5
+ * - list(req, res) β†’ listado paginado y filtrado de mercados activos.
6
+ * - getById(req, res) β†’ detalle de un mercado por su ID nativo de Polymarket.
7
+ *
8
+ * Datos expuestos:
9
+ * - id, question, category, countryCode, yesPrice, noPrice,
10
+ * volumeEur, liquidityEur, status, closesAt, lastSynced.
11
+ *
12
+ * Errores:
13
+ * - 404 NOT_FOUND si el mercado no existe.
14
+ */
15
+
16
  import { ok } from '../utils/apiResponse.js';
17
  import { marketsService } from './markets.service.js';
18
 
backend/src/markets/markets.repository.js CHANGED
@@ -1,3 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { prisma } from '../utils/prisma.js';
2
 
3
  export const marketsRepository = {
 
1
+ /**
2
+ * Repositorio de acceso a datos para el modelo Market.
3
+ *
4
+ * Responsabilidades:
5
+ * - findMany({ limit, offset, category, status }) β†’ listado paginado y filtrado.
6
+ * - count({ category, status }) β†’ conteo para paginacion.
7
+ * - findById(id) β†’ busqueda por ID nativo.
8
+ * - upsert(market) β†’ crea o actualiza un mercado.
9
+ *
10
+ * Todas las operaciones usan Prisma ORM (queries parametrizadas, sin SQL injection).
11
+ * El orden por defecto es descendente por volumen (volumeEur).
12
+ */
13
+
14
  import { prisma } from '../utils/prisma.js';
15
 
16
  export const marketsRepository = {
backend/src/markets/markets.routes.js CHANGED
@@ -1,3 +1,13 @@
 
 
 
 
 
 
 
 
 
 
1
  import { Router } from 'express';
2
  import { validate } from '../middlewares/validate.js';
3
  import { marketsController } from './markets.controller.js';
 
1
+ /**
2
+ * Rutas REST del modulo de mercados.
3
+ *
4
+ * Endpoints (montados en /api/v1/markets):
5
+ * GET / β†’ listado paginado/filtrado (validate listQuery en query).
6
+ * GET /:id β†’ detalle de mercado (validate idParam en params).
7
+ *
8
+ * No requieren autenticacion (datos publicos de Polymarket).
9
+ */
10
+
11
  import { Router } from 'express';
12
  import { validate } from '../middlewares/validate.js';
13
  import { marketsController } from './markets.controller.js';
backend/src/markets/markets.service.js CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { HttpError } from '../utils/apiResponse.js';
2
  import { marketsRepository } from './markets.repository.js';
3
 
 
1
+ /**
2
+ * Logica de negocio del modulo de mercados.
3
+ *
4
+ * Responsabilidades:
5
+ * - list(query) β†’ delega a marketsRepository.findMany + count para paginacion.
6
+ * - getById(id) β†’ busca un mercado; si no existe lanza 404.
7
+ *
8
+ * Consumido por:
9
+ * - markets.controller.js (API REST).
10
+ * - scheduler.js (syncMarkets cada 30s).
11
+ *
12
+ * No contiene logica de scraping directa; delega a polymarket.client.js
13
+ * y persiste via marketsRepository.
14
+ */
15
+
16
  import { HttpError } from '../utils/apiResponse.js';
17
  import { marketsRepository } from './markets.repository.js';
18
 
backend/src/markets/markets.validators.js CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import { z } from 'zod';
2
 
3
  export const listQuery = z.object({
 
1
+ /**
2
+ * Esquemas Zod para validar inputs del modulo de mercados.
3
+ *
4
+ * Responsabilidades:
5
+ * - listQuery β†’ limit (1-100, default 20), offset, category enum, status enum.
6
+ * - idParam β†’ string no vacio para el parametro :id.
7
+ *
8
+ * Consumido por:
9
+ * - markets.routes.js β†’ validate(listQuery, 'query') y validate(idParam, 'params').
10
+ */
11
+
12
  import { z } from 'zod';
13
 
14
  export const listQuery = z.object({
backend/src/markets/polymarket.client.js CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { httpGet } from '../utils/httpClient.js';
2
 
3
  const GAMMA_URL = 'https://gamma-api.polymarket.com/markets';
 
1
+ /**
2
+ * Cliente de integracion con Polymarket Gamma API.
3
+ *
4
+ * Responsabilidades:
5
+ * - fetchActiveMarkets(limit) β†’ obtiene mercados activos desde gamma-api.polymarket.com.
6
+ * - mapMarket(raw) β†’ normaliza campos crudos a la estructura del modelo Market:
7
+ * * id, question, category, countryCode
8
+ * * yesPrice / noPrice (parseados desde outcomePrices JSON)
9
+ * * volumeEur / liquidityEur (convertidos de USD a EUR con tasa 0.93)
10
+ * * status (active | closed | resolved) derivado de flags active/closed/archived
11
+ * * closesAt, lastSynced
12
+ *
13
+ * No requiere autenticacion. No usa CLOB API (solo datos publicos de mercado).
14
+ * Consumido por scheduler.js cada 30 segundos.
15
+ */
16
+
17
  import { httpGet } from '../utils/httpClient.js';
18
 
19
  const GAMMA_URL = 'https://gamma-api.polymarket.com/markets';
backend/src/middlewares/errorHandler.js CHANGED
@@ -1,3 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { HttpError } from '../utils/apiResponse.js';
2
  import { logger } from '../utils/logger.js';
3
 
 
1
+ /**
2
+ * Middleware centralizado de manejo de errores.
3
+ *
4
+ * Responsabilidades:
5
+ * - Capturar errores lanzados por controladores, servicios o middlewares.
6
+ * - Si es HttpError conocido: responder con el status y codigo definido.
7
+ * - Si es error inesperado: loguear el stack y responder 500 generico
8
+ * ("Internal server error") para no filtrar detalles internos en produccion.
9
+ *
10
+ * Montado al final de app.js como ultimo middleware.
11
+ */
12
+
13
  import { HttpError } from '../utils/apiResponse.js';
14
  import { logger } from '../utils/logger.js';
15
 
backend/src/middlewares/notFound.js CHANGED
@@ -1,3 +1,13 @@
 
 
 
 
 
 
 
 
 
 
1
  export const notFound = (req, res) =>
2
  res.status(404).json({
3
  ok: false,
 
1
+ /**
2
+ * Middleware para rutas no definidas (404).
3
+ *
4
+ * Responsabilidades:
5
+ * - Responder con 404 y un JSON estandarizado cuando ninguna ruta coincide.
6
+ * - Incluye el metodo y path solicitado para facilitar debugging.
7
+ *
8
+ * Montado al final de app.js, justo antes del errorHandler.
9
+ */
10
+
11
  export const notFound = (req, res) =>
12
  res.status(404).json({
13
  ok: false,
backend/src/middlewares/rateLimitLogin.js CHANGED
@@ -1,3 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import rateLimit from 'express-rate-limit';
2
 
3
  export const rateLimitLogin = rateLimit({
 
1
+ /**
2
+ * Rate limiter especifico para el endpoint de login.
3
+ *
4
+ * Responsabilidades:
5
+ * - Limitar a 5 intentos de login cada 15 minutos por IP.
6
+ * - Mitigar ataques de fuerza bruta contra credenciales.
7
+ * - Responder 429 con codigo TOO_MANY_REQUESTS cuando se excede.
8
+ *
9
+ * Aplicado unicamente en POST /api/v1/auth/login.
10
+ * El rate limiter global (200 req / 15 min) tambien protege el resto de la API.
11
+ */
12
+
13
  import rateLimit from 'express-rate-limit';
14
 
15
  export const rateLimitLogin = rateLimit({
backend/src/middlewares/requireAuth.js CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { verifyToken } from '../auth/jwt.js';
2
  import { prisma } from '../utils/prisma.js';
3
  import { HttpError } from '../utils/apiResponse.js';
 
1
+ /**
2
+ * Middleware de autenticacion JWT.
3
+ *
4
+ * Responsabilidades:
5
+ * - Extraer el header Authorization: Bearer <token>.
6
+ * - Verificar la firma y expiracion del token con jwt.verify().
7
+ * - Buscar el usuario en la base de datos y comprobar que esta activo (isActive).
8
+ * - Adjuntar req.user para que controladores y servicios posteriores lo usen.
9
+ *
10
+ * Rutas protegidas:
11
+ * - Todas bajo /positions, /watchlist, /alerts.
12
+ * - GET /auth/me.
13
+ *
14
+ * Si falta token, es invalido o el usuario no existe/inactivo β†’ 401 UNAUTHORIZED.
15
+ */
16
+
17
  import { verifyToken } from '../auth/jwt.js';
18
  import { prisma } from '../utils/prisma.js';
19
  import { HttpError } from '../utils/apiResponse.js';
backend/src/middlewares/validate.js CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { HttpError } from '../utils/apiResponse.js';
2
 
3
  export const validate = (schema, source = 'body') => (req, _res, next) => {
 
1
+ /**
2
+ * Middleware generico de validacion de inputs con Zod.
3
+ *
4
+ * Responsabilidades:
5
+ * - Ejecutar schema.safeParse() sobre el source indicado (body, query, params).
6
+ * - Si falla: propagar HttpError 400 con los errores de validacion detallados.
7
+ * - Si pasa: reemplazar req[source] por los datos parseados (coercion incluida).
8
+ *
9
+ * Uso tipico:
10
+ * router.post('/', validate(createBody), controller.create);
11
+ * router.get('/', validate(listQuery, 'query'), controller.list);
12
+ * router.get('/:id', validate(idParam, 'params'), controller.getById);
13
+ *
14
+ * Fuente por defecto: 'body'. Para query y params debe especificarse explicitamente.
15
+ */
16
+
17
  import { HttpError } from '../utils/apiResponse.js';
18
 
19
  export const validate = (schema, source = 'body') => (req, _res, next) => {
backend/src/positions/kelly.js CHANGED
@@ -1,7 +1,20 @@
1
  /**
2
- * Kelly criterion capped at 25%.
3
- * k = (odds * pWin - pLose) / odds
4
- * odds = (1 / price) - 1
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  */
6
  export function kellyFraction(price, pWin) {
7
  if (!price || price <= 0 || price >= 1 || !pWin) return 0;
 
1
  /**
2
+ * Criterio de Kelly para sizing de posiciones, limitado al 25%.
3
+ *
4
+ * Formula:
5
+ * odds = (1 / price) - 1
6
+ * k = (odds * pWin - pLose) / odds
7
+ * return min(max(k, 0), 0.25)
8
+ *
9
+ * Parametros:
10
+ * - price : precio actual del outcome (0 < price < 1).
11
+ * - pWin : probabilidad de ganar (usamos confidence de la senal IA).
12
+ *
13
+ * Devuelve:
14
+ * Fraccion del capital virtual recomendada (0.0 a 0.25).
15
+ *
16
+ * Consumido por:
17
+ * - positions.service.js β†’ al abrir una posicion.
18
  */
19
  export function kellyFraction(price, pWin) {
20
  if (!price || price <= 0 || price >= 1 || !pWin) return 0;
backend/src/positions/positions.controller.js CHANGED
@@ -1,3 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { ok, created, noContent } from '../utils/apiResponse.js';
2
  import { positionsService } from './positions.service.js';
3
 
 
1
+ /**
2
+ * Controladores del modulo de posiciones (simulador virtual).
3
+ *
4
+ * Responsabilidades:
5
+ * - open(req, res) β†’ abre una posicion virtual en un mercado.
6
+ * - list(req, res) β†’ lista las posiciones del usuario autenticado.
7
+ * - close(req, res) β†’ cierra una posicion y calcula P&L final.
8
+ *
9
+ * Endpoints (bajo /api/v1/positions, protegidos por requireAuth):
10
+ * POST / β†’ abrir posicion.
11
+ * GET / β†’ listar posiciones (opcional ?status=open|closed).
12
+ * DELETE /:id β†’ cerrar posicion.
13
+ */
14
+
15
  import { ok, created, noContent } from '../utils/apiResponse.js';
16
  import { positionsService } from './positions.service.js';
17
 
backend/src/positions/positions.repository.js CHANGED
@@ -1,3 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { prisma } from '../utils/prisma.js';
2
 
3
  export const positionsRepository = {
 
1
+ /**
2
+ * Repositorio de acceso a datos para el modelo Position.
3
+ *
4
+ * Responsabilidades:
5
+ * - create(data) β†’ inserta una nueva posicion virtual.
6
+ * - findByUser(userId, status) β†’ lista posiciones del usuario con datos del mercado.
7
+ * - findByIdAndUser(id, userId) β†’ busca posicion propia del usuario.
8
+ * - findAllOpen() β†’ todas las posiciones abiertas (para recalcular P&L).
9
+ * - update(id, data) β†’ actualiza precio, P&L, estado, etc.
10
+ *
11
+ * Todas las operaciones usan Prisma ORM.
12
+ */
13
+
14
  import { prisma } from '../utils/prisma.js';
15
 
16
  export const positionsRepository = {
backend/src/positions/positions.routes.js CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import { Router } from 'express';
2
  import { validate } from '../middlewares/validate.js';
3
  import { requireAuth } from '../middlewares/requireAuth.js';
 
1
+ /**
2
+ * Rutas REST del modulo de posiciones (simulador virtual).
3
+ *
4
+ * Endpoints (montados en /api/v1/positions):
5
+ * POST / β†’ abrir posicion (validate openBody).
6
+ * GET / β†’ listar posiciones (validate listQuery en query).
7
+ * DELETE /:id β†’ cerrar posicion (validate idParam en params).
8
+ *
9
+ * Todas las rutas requieren autenticacion JWT (router.use(requireAuth)).
10
+ */
11
+
12
  import { Router } from 'express';
13
  import { validate } from '../middlewares/validate.js';
14
  import { requireAuth } from '../middlewares/requireAuth.js';
backend/src/positions/positions.service.js CHANGED
@@ -1,3 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { HttpError } from '../utils/apiResponse.js';
2
  import { positionsRepository } from './positions.repository.js';
3
  import { marketsRepository } from '../markets/markets.repository.js';
 
1
+ /**
2
+ * Logica de negocio del modulo de posiciones (simulador virtual).
3
+ *
4
+ * Responsabilidades:
5
+ * - open(userId, { marketId, outcome, amountEur })
6
+ * β†’ valida que el mercado exista y este activo, obtiene precio de entrada,
7
+ * calcula fraccion de Kelly y crea la posicion.
8
+ * - list(userId, status) β†’ devuelve posiciones del usuario.
9
+ * - close(id, userId) β†’ calcula P&L final con el precio actual y marca como cerrada.
10
+ * - updateAllPnL() β†’ recalcula P&L de todas las posiciones abiertas (scheduler).
11
+ *
12
+ * Calculos:
13
+ * - P&L = amountEur * (currentPrice / entryPrice - 1).
14
+ * - Kelly = (odds * pWin - pLose) / odds, capped al 25%.
15
+ *
16
+ * Consumido por:
17
+ * - positions.controller.js (API REST).
18
+ * - scheduler.js (updatePositionsPnL cada 30s).
19
+ */
20
+
21
  import { HttpError } from '../utils/apiResponse.js';
22
  import { positionsRepository } from './positions.repository.js';
23
  import { marketsRepository } from '../markets/markets.repository.js';
backend/src/positions/positions.validators.js CHANGED
@@ -1,3 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { z } from 'zod';
2
 
3
  export const openBody = z.object({
 
1
+ /**
2
+ * Esquemas Zod para validar inputs del modulo de posiciones.
3
+ *
4
+ * Responsabilidades:
5
+ * - openBody β†’ marketId (string), outcome ('YES'|'NO'), amountEur (positivo, max 100k).
6
+ * - idParam β†’ coerce a numero entero positivo.
7
+ * - listQuery β†’ status opcional ('open'|'closed').
8
+ *
9
+ * Consumido por:
10
+ * - positions.routes.js β†’ validate() en POST, GET y DELETE.
11
+ */
12
+
13
  import { z } from 'zod';
14
 
15
  export const openBody = z.object({
backend/src/scheduler.js CHANGED
@@ -1,3 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { schedule } from 'node-cron';
2
  import { fetchActiveMarkets } from './markets/polymarket.client.js';
3
  import { marketsRepository } from './markets/markets.repository.js';
 
1
+ /**
2
+ * Scheduler de tareas periodicas usando node-cron.
3
+ *
4
+ * Define y ejecuta los 4 jobs principales del backend:
5
+ * 1. syncMarkets β€” cada 30s, sincroniza precios desde Polymarket Gamma API.
6
+ * 2. generateSignals β€” cada 5 min, genera senales IA para el top 20 mercados activos.
7
+ * 3. updatePositionsPnL β€” cada 30s, recalcula P&L de posiciones abiertas.
8
+ * 4. processAlerts β€” cada 60s, revisa watchlist y envia alertas por Telegram.
9
+ *
10
+ * Cada job captura sus propios errores para evitar que un fallo afecte a los demas.
11
+ * Se ejecuta inmediatamente al arrancar (llamada manual) y luego segun cron.
12
+ */
13
+
14
  import { schedule } from 'node-cron';
15
  import { fetchActiveMarkets } from './markets/polymarket.client.js';
16
  import { marketsRepository } from './markets/markets.repository.js';
backend/src/signals/aiPipeline.js CHANGED
@@ -1,3 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { httpPost } from '../utils/httpClient.js';
2
  import { config } from '../config.js';
3
  import { logger } from '../utils/logger.js';
@@ -9,6 +29,10 @@ const QWEN_MODEL = 'Qwen/Qwen3-8B';
9
  const OPENROUTER_API = 'https://openrouter.ai/api/v1/chat/completions';
10
  const OPENROUTER_MODEL = 'deepseek/deepseek-chat-v3-0324:free';
11
 
 
 
 
 
12
  // ── helpers ──────────────────────────────────────────────────────────────────
13
 
14
  function hfHeaders() {
@@ -43,9 +67,42 @@ function buildPrompt(market, headlines) {
43
  ].join('\n');
44
  }
45
 
46
- // ── FinBERT filtering ─────────────────────────────────────────────────────────
47
 
48
- async function filterWithFinBERT(articles) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  if (!articles.length) return articles;
50
  const inputs = articles.map((a) => a.headline);
51
  const results = await httpPost(
@@ -60,7 +117,44 @@ async function filterWithFinBERT(articles) {
60
  });
61
  }
62
 
63
- // ── LLM generation ────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  async function callChatCompletion(url, model, messages, authHeader) {
66
  const data = await httpPost(
@@ -71,7 +165,7 @@ async function callChatCompletion(url, model, messages, authHeader) {
71
  return data?.choices?.[0]?.message?.content ?? null;
72
  }
73
 
74
- async function generateWithQwen3(market, headlines) {
75
  const content = await callChatCompletion(
76
  `${HF_API}/${QWEN_MODEL}/v1/chat/completions`,
77
  QWEN_MODEL,
@@ -119,7 +213,7 @@ function ruleBasedSignal(market) {
119
 
120
  function validateSignal(result) {
121
  if (!result) return false;
122
- const validSignals = ['bullish', 'bearish', 'neutral'];
123
  return (
124
  validSignals.includes(result.signal) &&
125
  typeof result.confidence === 'number' &&
@@ -130,6 +224,16 @@ function validateSignal(result) {
130
  );
131
  }
132
 
 
 
 
 
 
 
 
 
 
 
133
  // ── public API ────────────────────────────────────────────────────────────────
134
 
135
  export async function run(market) {
@@ -138,10 +242,10 @@ export async function run(market) {
138
  try {
139
  const allNews = await fetchFinancialNews(30);
140
  const relevant = filterNewsByRelevance(allNews, market.question);
141
- if (config.HF_TOKEN && relevant.length) {
142
  headlines = await filterWithFinBERT(relevant);
143
  } else {
144
- headlines = relevant.slice(0, 10);
145
  }
146
  } catch (err) {
147
  logger.warn({ err: err.message, marketId: market.id }, 'news fetch failed, continuing without news');
@@ -150,15 +254,28 @@ export async function run(market) {
150
  // Step 2: LLM signal generation with fallback chain
151
  let result = null;
152
 
153
- if (config.HF_TOKEN) {
 
 
 
 
 
 
 
 
 
 
 
 
154
  try {
155
- result = await generateWithQwen3(market, headlines);
156
  if (!validateSignal(result)) result = null;
157
  } catch (err) {
158
- logger.warn({ err: err.message, marketId: market.id }, 'Qwen3 failed, trying OpenRouter');
159
  }
160
  }
161
 
 
162
  if (!result && config.OPENROUTER_API_KEY) {
163
  try {
164
  result = await generateWithOpenRouter(market, headlines);
@@ -168,6 +285,7 @@ export async function run(market) {
168
  }
169
  }
170
 
 
171
  if (!result) {
172
  result = ruleBasedSignal(market);
173
  }
 
1
+ /**
2
+ * Pipeline de inteligencia artificial para generacion de senales de trading.
3
+ *
4
+ * Flujo de tres fases con fallback:
5
+ * 1. Filtrado de noticias (Finnhub + ModernFinBERT Space o API directa HF).
6
+ * β†’ descarta noticias neutrales o con score < 0.65.
7
+ * 2. Generacion de senal (Qwen3-8B Space o API directa HF).
8
+ * β†’ analiza mercado + noticias filtradas y genera:
9
+ * { signal: 'bullish'|'bearish'|'neutral', confidence, summary, keyRisk }.
10
+ * 3. Fallbacks:
11
+ * - Si falla el Space de Qwen β†’ intenta API directa de HuggingFace.
12
+ * - Si falla HF directa β†’ intenta OpenRouter (deepseek-chat).
13
+ * - Si todo falla β†’ regla basada en precio (rule-based).
14
+ *
15
+ * Consumido por:
16
+ * - signals.service.js β†’ generateForMarket(market).
17
+ * - scheduler.js β†’ cada 5 minutos para los 20 mercados activos.
18
+ */
19
+
20
+ import { Client } from '@gradio/client';
21
  import { httpPost } from '../utils/httpClient.js';
22
  import { config } from '../config.js';
23
  import { logger } from '../utils/logger.js';
 
29
  const OPENROUTER_API = 'https://openrouter.ai/api/v1/chat/completions';
30
  const OPENROUTER_MODEL = 'deepseek/deepseek-chat-v3-0324:free';
31
 
32
+ // Cached Gradio clients for Spaces
33
+ let modernFinBERTClient = null;
34
+ let qwenClient = null;
35
+
36
  // ── helpers ──────────────────────────────────────────────────────────────────
37
 
38
  function hfHeaders() {
 
67
  ].join('\n');
68
  }
69
 
70
+ // ── Gradio Spaces clients ────────────────────────────────────────────────────
71
 
72
+ async function getModernFinBERTClient() {
73
+ if (!modernFinBERTClient && config.HF_SPACE_MODERNFINBERT_URL) {
74
+ modernFinBERTClient = await Client.connect(config.HF_SPACE_MODERNFINBERT_URL);
75
+ }
76
+ return modernFinBERTClient;
77
+ }
78
+
79
+ async function getQwenClient() {
80
+ if (!qwenClient && config.HF_SPACE_QWEN_URL) {
81
+ qwenClient = await Client.connect(config.HF_SPACE_QWEN_URL);
82
+ }
83
+ return qwenClient;
84
+ }
85
+
86
+ // ── FinBERT filtering via Space or direct API ─────────────────────────────────
87
+
88
+ async function filterWithFinBERTSpace(articles) {
89
+ if (!articles.length) return articles;
90
+ const client = await getModernFinBERTClient();
91
+ if (!client) return articles;
92
+
93
+ const textBlock = articles.map((a) => a.headline).join('\n');
94
+ const result = await client.predict('/predict', { text_block: textBlock });
95
+ // Gradio returns: { data: [ [ {label, score}, ... ] ], ... }
96
+ const sentiments = result.data[0];
97
+ return articles.filter((_, i) => {
98
+ const r = sentiments[i];
99
+ if (!r) return false;
100
+ const label = String(r.label).toLowerCase();
101
+ return label !== 'neutral' && (typeof r.score === 'number' ? r.score : parseFloat(r.score)) >= 0.65;
102
+ });
103
+ }
104
+
105
+ async function filterWithFinBERTDirect(articles) {
106
  if (!articles.length) return articles;
107
  const inputs = articles.map((a) => a.headline);
108
  const results = await httpPost(
 
117
  });
118
  }
119
 
120
+ async function filterWithFinBERT(articles) {
121
+ // Prefer Space if configured, fallback to direct API
122
+ if (config.HF_SPACE_MODERNFINBERT_URL) {
123
+ try {
124
+ return await filterWithFinBERTSpace(articles);
125
+ } catch (err) {
126
+ logger.warn({ err: err.message }, 'ModernFinBERT Space failed, falling back to direct API');
127
+ }
128
+ }
129
+ if (config.HF_TOKEN) {
130
+ return await filterWithFinBERTDirect(articles);
131
+ }
132
+ return articles;
133
+ }
134
+
135
+ // ── LLM generation via Space or direct API ───────────────────────────────────
136
+
137
+ async function generateWithQwenSpace(market, headlines) {
138
+ const client = await getQwenClient();
139
+ if (!client) return null;
140
+
141
+ const newsSummary = headlines.length
142
+ ? headlines.map((h) => `- ${h.headline}`).join('\n')
143
+ : 'No relevant news available.';
144
+
145
+ const marketContext = [
146
+ `Market: "${market.question}"`,
147
+ `YES price: ${market.yesPrice ?? 'N/A'} | NO price: ${market.noPrice ?? 'N/A'}`,
148
+ ].join('\n');
149
+
150
+ const result = await client.predict('/predict', {
151
+ market_context: marketContext,
152
+ news_summary: newsSummary,
153
+ });
154
+
155
+ // result.data[0] β†’ { signal, confidence, summary, keyRisk }
156
+ return result.data[0];
157
+ }
158
 
159
  async function callChatCompletion(url, model, messages, authHeader) {
160
  const data = await httpPost(
 
165
  return data?.choices?.[0]?.message?.content ?? null;
166
  }
167
 
168
+ async function generateWithQwen3Direct(market, headlines) {
169
  const content = await callChatCompletion(
170
  `${HF_API}/${QWEN_MODEL}/v1/chat/completions`,
171
  QWEN_MODEL,
 
213
 
214
  function validateSignal(result) {
215
  if (!result) return false;
216
+ const validSignals = ['bullish', 'bearish', 'neutral', 'buy', 'sell', 'hold'];
217
  return (
218
  validSignals.includes(result.signal) &&
219
  typeof result.confidence === 'number' &&
 
224
  );
225
  }
226
 
227
+ function normalizeSignal(result) {
228
+ if (!result) return result;
229
+ // Normalize buy/sell/hold to bullish/bearish/neutral for downstream compatibility
230
+ const signalMap = { buy: 'bullish', sell: 'bearish', hold: 'neutral' };
231
+ if (result.signal in signalMap) {
232
+ result.signal = signalMap[result.signal];
233
+ }
234
+ return result;
235
+ }
236
+
237
  // ── public API ────────────────────────────────────────────────────────────────
238
 
239
  export async function run(market) {
 
242
  try {
243
  const allNews = await fetchFinancialNews(30);
244
  const relevant = filterNewsByRelevance(allNews, market.question);
245
+ if (relevant.length) {
246
  headlines = await filterWithFinBERT(relevant);
247
  } else {
248
+ headlines = [];
249
  }
250
  } catch (err) {
251
  logger.warn({ err: err.message, marketId: market.id }, 'news fetch failed, continuing without news');
 
254
  // Step 2: LLM signal generation with fallback chain
255
  let result = null;
256
 
257
+ // Try Space first
258
+ if (config.HF_SPACE_QWEN_URL) {
259
+ try {
260
+ result = await generateWithQwenSpace(market, headlines);
261
+ result = normalizeSignal(result);
262
+ if (!validateSignal(result)) result = null;
263
+ } catch (err) {
264
+ logger.warn({ err: err.message, marketId: market.id }, 'Qwen Space failed, trying direct API');
265
+ }
266
+ }
267
+
268
+ // Fallback to direct HF API
269
+ if (!result && config.HF_TOKEN) {
270
  try {
271
+ result = await generateWithQwen3Direct(market, headlines);
272
  if (!validateSignal(result)) result = null;
273
  } catch (err) {
274
+ logger.warn({ err: err.message, marketId: market.id }, 'Qwen3 direct API failed, trying OpenRouter');
275
  }
276
  }
277
 
278
+ // Fallback to OpenRouter
279
  if (!result && config.OPENROUTER_API_KEY) {
280
  try {
281
  result = await generateWithOpenRouter(market, headlines);
 
285
  }
286
  }
287
 
288
+ // Final fallback
289
  if (!result) {
290
  result = ruleBasedSignal(market);
291
  }
backend/src/signals/finnhub.client.js CHANGED
@@ -1,3 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { httpGet } from '../utils/httpClient.js';
2
  import { config } from '../config.js';
3
 
 
1
+ /**
2
+ * Servicio de integracion con Finnhub REST API.
3
+ *
4
+ * Responsabilidades:
5
+ * - fetchFinancialNews(count) β†’ obtiene titulares de noticias financieras.
6
+ * - filterNewsByRelevance(articles, question) β†’ filtra por keywords del mercado.
7
+ *
8
+ * Restricciones:
9
+ * - Free tier: maximo 60 llamadas/minuto.
10
+ * - Solo se invoca durante la generacion de senales (cada 5 min).
11
+ * - Si FINNHUB_API_KEY no esta configurado, devuelve array vacio.
12
+ *
13
+ * Consumido por:
14
+ * - aiPipeline.js β†’ fase 1 de filtrado de noticias.
15
+ */
16
+
17
  import { httpGet } from '../utils/httpClient.js';
18
  import { config } from '../config.js';
19
 
backend/src/signals/signals.controller.js CHANGED
@@ -1,3 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { ok } from '../utils/apiResponse.js';
2
  import { signalsService } from './signals.service.js';
3
 
 
1
+ /**
2
+ * Controladores del modulo de senales IA.
3
+ *
4
+ * Responsabilidades:
5
+ * - getLatest(req, res) β†’ devuelve la senal mas reciente de un mercado.
6
+ *
7
+ * Endpoint:
8
+ * GET /api/v1/markets/:marketId/signal
9
+ *
10
+ * Errores:
11
+ * - 404 NOT_FOUND si el mercado no existe.
12
+ * - 404 NOT_FOUND si aun no hay senal generada para ese mercado.
13
+ */
14
+
15
  import { ok } from '../utils/apiResponse.js';
16
  import { signalsService } from './signals.service.js';
17
 
backend/src/signals/signals.repository.js CHANGED
@@ -1,3 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { prisma } from '../utils/prisma.js';
2
 
3
  export const signalsRepository = {
 
1
+ /**
2
+ * Repositorio de acceso a datos para el modelo AISignal.
3
+ *
4
+ * Responsabilidades:
5
+ * - create(data) β†’ inserta una nueva senal generada por IA.
6
+ * - findLatestByMarket(id) β†’ devuelve la senal mas reciente de un mercado.
7
+ *
8
+ * Campos persistidos:
9
+ * signal, confidence, summary, keyRisk, newsCount, modelVersion, generatedAt.
10
+ *
11
+ * Todas las operaciones usan Prisma ORM.
12
+ */
13
+
14
  import { prisma } from '../utils/prisma.js';
15
 
16
  export const signalsRepository = {
backend/src/signals/signals.routes.js CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import { Router } from 'express';
2
  import { signalsController } from './signals.controller.js';
3
 
 
1
+ /**
2
+ * Rutas REST del modulo de senales IA.
3
+ *
4
+ * Endpoint (montado en /api/v1/markets):
5
+ * GET /:marketId/signal β†’ senal mas reciente del mercado.
6
+ *
7
+ * No requiere autenticacion (datos publicos).
8
+ */
9
+
10
  import { Router } from 'express';
11
  import { signalsController } from './signals.controller.js';
12
 
backend/src/signals/signals.service.js CHANGED
@@ -1,3 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { HttpError } from '../utils/apiResponse.js';
2
  import { signalsRepository } from './signals.repository.js';
3
  import { marketsRepository } from '../markets/markets.repository.js';
 
1
+ /**
2
+ * Logica de negocio del modulo de senales IA.
3
+ *
4
+ * Responsabilidades:
5
+ * - getLatest(marketId) β†’ devuelve la ultima senal persistida de un mercado.
6
+ * - generateForMarket(market) β†’ ejecuta el pipeline de IA (aiPipeline.js) y persiste
7
+ * el resultado en AISignal. Emite evento 'ai_signal' por Socket.io.
8
+ *
9
+ * Flujo de generacion:
10
+ * 1. aiPipeline.run(market) β†’ obtiene { signal, confidence, summary, keyRisk, newsCount }.
11
+ * 2. signalsRepository.create(...) β†’ persiste en SQLite.
12
+ * 3. emitAiSignal(...) β†’ notifica a clientes conectados en tiempo real.
13
+ *
14
+ * Consumido por:
15
+ * - scheduler.js cada 5 minutos para los 20 mercados mas activos.
16
+ * - signals.controller.js para el endpoint GET /markets/:id/signal.
17
+ */
18
+
19
  import { HttpError } from '../utils/apiResponse.js';
20
  import { signalsRepository } from './signals.repository.js';
21
  import { marketsRepository } from '../markets/markets.repository.js';
backend/src/socket/broadcaster.js CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  let _io = null;
2
 
3
  export function attachBroadcaster(io) {
 
1
+ /**
2
+ * Broadcaster de eventos en tiempo real via Socket.io.
3
+ *
4
+ * Actua como un wrapper singleton sobre la instancia de Socket.io.
5
+ * Responsabilidades:
6
+ * - Recibir la instancia de io en el arranque (attachBroadcaster).
7
+ * - Emitir eventos tipados a todos los clientes conectados:
8
+ * * market_update β†’ cambio de precio/volumen de un mercado.
9
+ * * ai_signal β†’ nueva senal generada por el pipeline de IA.
10
+ * * price_alert β†’ alerta de umbral de precio o cambio de senal.
11
+ *
12
+ * Los controladores y servicios importan estas funciones para notificar
13
+ * sin acoplarse directamente a Socket.io.
14
+ */
15
+
16
  let _io = null;
17
 
18
  export function attachBroadcaster(io) {
backend/src/utils/apiResponse.js CHANGED
@@ -1,3 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  export const ok = (res, data, meta) =>
2
  res.status(200).json(meta ? { ok: true, data, meta } : { ok: true, data });
3
 
 
1
+ /**
2
+ * Helpers para respuestas HTTP estandarizadas y manejo de errores.
3
+ *
4
+ * Funciones de respuesta exitosa:
5
+ * - ok(res, data, meta) β†’ 200 con { ok: true, data } y meta opcional.
6
+ * - created(res, data) β†’ 201 con { ok: true, data }.
7
+ * - noContent(res) β†’ 204 sin body.
8
+ *
9
+ * Clase HttpError:
10
+ * - Extiende Error nativo.
11
+ * - Campos: status (HTTP), code (string identificador), message, details (opcional).
12
+ * - Usada por middlewares y servicios para propagar errores controlados.
13
+ */
14
+
15
  export const ok = (res, data, meta) =>
16
  res.status(200).json(meta ? { ok: true, data, meta } : { ok: true, data });
17
 
backend/src/utils/httpClient.js CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { logger } from './logger.js';
2
 
3
  const DEFAULT_TIMEOUT_MS = 10_000;
 
1
+ /**
2
+ * Cliente HTTP reutilizable con retry y timeout.
3
+ *
4
+ * Responsabilidades:
5
+ * - Realizar peticiones GET y POST usando fetch nativo de Node.js.
6
+ * - Aplicar timeout via AbortController.
7
+ * - Reintentar automaticamente ante errores de red o HTTP 429/5xx.
8
+ * - Backoff exponencial con tope de 30s.
9
+ * - Inyectar User-Agent: PolySignal/1.0 en todas las peticiones.
10
+ *
11
+ * Uso:
12
+ * const data = await httpGet(url, { headers, timeout, retries });
13
+ * const data = await httpPost(url, body, { headers, timeout, retries });
14
+ */
15
+
16
  import { logger } from './logger.js';
17
 
18
  const DEFAULT_TIMEOUT_MS = 10_000;
backend/src/utils/logger.js CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import pino from 'pino';
2
 
3
  export const logger = pino({
 
1
+ /**
2
+ * Logger estructurado basado en Pino.
3
+ *
4
+ * Responsabilidades:
5
+ * - Loguear eventos del servidor en formato JSON (facil de parsear).
6
+ * - Nivel configurable via LOG_LEVEL (trace, debug, info, warn, error, fatal).
7
+ * - Usado por todos los modulos para trazabilidad sin bloquear el event loop.
8
+ *
9
+ * En produccion se puede redirigir stdout a un agregador de logs (Datadog, etc.).
10
+ */
11
+
12
  import pino from 'pino';
13
 
14
  export const logger = pino({
backend/src/utils/prisma.js CHANGED
@@ -1,3 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { PrismaClient } from '@prisma/client';
2
 
3
  const globalForPrisma = globalThis;
 
1
+ /**
2
+ * Instancia singleton de PrismaClient.
3
+ *
4
+ * Responsabilidades:
5
+ * - Gestionar la conexion a SQLite (desarrollo) o PostgreSQL (produccion).
6
+ * - Reutilizar la misma instancia en hot-reload (guardada en globalThis).
7
+ * - Loguear solo warns y errores para no saturar la salida.
8
+ *
9
+ * Relaciones:
10
+ * - Todos los repositories importan este prisma para ejecutar queries.
11
+ * - schema.prisma define los modelos: User, Market, AISignal, Position, Watchlist, Alert.
12
+ */
13
+
14
  import { PrismaClient } from '@prisma/client';
15
 
16
  const globalForPrisma = globalThis;
backend/src/watchlist/watchlist.controller.js CHANGED
@@ -1,3 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { ok, created, noContent } from '../utils/apiResponse.js';
2
  import { watchlistService } from './watchlist.service.js';
3
 
 
1
+ /**
2
+ * Controladores del modulo de lista de seguimiento (watchlist).
3
+ *
4
+ * Responsabilidades:
5
+ * - add(req, res) β†’ anade un mercado a la watchlist con umbral de alerta opcional.
6
+ * - remove(req, res) β†’ elimina un mercado de la watchlist.
7
+ * - list(req, res) β†’ devuelve la watchlist del usuario autenticado.
8
+ *
9
+ * Endpoints (bajo /api/v1/watchlist, protegidos por requireAuth):
10
+ * POST / β†’ anadir mercado.
11
+ * GET / β†’ listar watchlist.
12
+ * DELETE /:marketId β†’ eliminar mercado.
13
+ */
14
+
15
  import { ok, created, noContent } from '../utils/apiResponse.js';
16
  import { watchlistService } from './watchlist.service.js';
17
 
backend/src/watchlist/watchlist.repository.js CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { prisma } from '../utils/prisma.js';
2
 
3
  export const watchlistRepository = {
 
1
+ /**
2
+ * Repositorio de acceso a datos para el modelo Watchlist.
3
+ *
4
+ * Responsabilidades:
5
+ * - create(data) β†’ inserta entrada en watchlist.
6
+ * - findByUser(userId) β†’ lista con datos del mercado asociado.
7
+ * - deleteByUserAndMarket(...) β†’ elimina entrada especifica.
8
+ * - findAllWithThreshold() β†’ entradas con alertThreshold definido (para alertas).
9
+ *
10
+ * Constraint:
11
+ * - @@unique([userId, marketId]) β†’ un usuario no puede duplicar un mercado.
12
+ *
13
+ * Todas las operaciones usan Prisma ORM.
14
+ */
15
+
16
  import { prisma } from '../utils/prisma.js';
17
 
18
  export const watchlistRepository = {
backend/src/watchlist/watchlist.routes.js CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import { Router } from 'express';
2
  import { validate } from '../middlewares/validate.js';
3
  import { requireAuth } from '../middlewares/requireAuth.js';
 
1
+ /**
2
+ * Rutas REST del modulo de lista de seguimiento (watchlist).
3
+ *
4
+ * Endpoints (montados en /api/v1/watchlist):
5
+ * POST / β†’ anadir mercado (validate addBody).
6
+ * GET / β†’ listar watchlist.
7
+ * DELETE /:marketId β†’ eliminar mercado (validate marketIdParam en params).
8
+ *
9
+ * Todas las rutas requieren autenticacion JWT (router.use(requireAuth)).
10
+ */
11
+
12
  import { Router } from 'express';
13
  import { validate } from '../middlewares/validate.js';
14
  import { requireAuth } from '../middlewares/requireAuth.js';
backend/src/watchlist/watchlist.service.js CHANGED
@@ -1,3 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import { HttpError } from '../utils/apiResponse.js';
2
  import { watchlistRepository } from './watchlist.repository.js';
3
  import { marketsRepository } from '../markets/markets.repository.js';
 
1
+ /**
2
+ * Logica de negocio del modulo de lista de seguimiento (watchlist).
3
+ *
4
+ * Responsabilidades:
5
+ * - add(userId, { marketId, alertThreshold })
6
+ * β†’ valida que el mercado exista, crea entrada en watchlist.
7
+ * β†’ si ya existe (P2002), devuelve 409 ALREADY_IN_WATCHLIST.
8
+ * - remove(userId, marketId) β†’ elimina entrada; si no existe devuelve 404.
9
+ * - list(userId) β†’ devuelve todas las entradas del usuario.
10
+ *
11
+ * Consumido por:
12
+ * - watchlist.controller.js (API REST).
13
+ * - alerts.service.js β†’ processAll() revisa watchlist con alertThreshold.
14
+ */
15
+
16
  import { HttpError } from '../utils/apiResponse.js';
17
  import { watchlistRepository } from './watchlist.repository.js';
18
  import { marketsRepository } from '../markets/markets.repository.js';
backend/src/watchlist/watchlist.validators.js CHANGED
@@ -1,3 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
1
  import { z } from 'zod';
2
 
3
  export const addBody = z.object({
 
1
+ /**
2
+ * Esquemas Zod para validar inputs del modulo de watchlist.
3
+ *
4
+ * Responsabilidades:
5
+ * - addBody β†’ marketId (string), alertThreshold opcional (0.0 a 1.0).
6
+ * - marketIdParam β†’ string no vacio para el parametro :marketId.
7
+ *
8
+ * Consumido por:
9
+ * - watchlist.routes.js β†’ validate() en POST y DELETE.
10
+ */
11
+
12
  import { z } from 'zod';
13
 
14
  export const addBody = z.object({