Spaces:
Sleeping
Sleeping
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 filesThis view is limited to 50 files because it contains too many changes. Β See raw diff
- .env.example +18 -6
- README.md +170 -42
- backend/package.json +1 -0
- backend/src/alerts/alerts.controller.js +10 -0
- backend/src/alerts/alerts.repository.js +14 -0
- backend/src/alerts/alerts.routes.js +9 -0
- backend/src/alerts/alerts.service.js +21 -0
- backend/src/alerts/telegram.client.js +17 -0
- backend/src/app.js +23 -0
- backend/src/auth/auth.controller.js +13 -0
- backend/src/auth/auth.routes.js +16 -0
- backend/src/auth/auth.service.js +17 -0
- backend/src/auth/auth.validators.js +11 -0
- backend/src/auth/jwt.js +16 -0
- backend/src/config.js +21 -0
- backend/src/index.js +17 -0
- backend/src/markets/markets.controller.js +15 -0
- backend/src/markets/markets.repository.js +13 -0
- backend/src/markets/markets.routes.js +10 -0
- backend/src/markets/markets.service.js +15 -0
- backend/src/markets/markets.validators.js +11 -0
- backend/src/markets/polymarket.client.js +16 -0
- backend/src/middlewares/errorHandler.js +12 -0
- backend/src/middlewares/notFound.js +10 -0
- backend/src/middlewares/rateLimitLogin.js +12 -0
- backend/src/middlewares/requireAuth.js +16 -0
- backend/src/middlewares/validate.js +16 -0
- backend/src/positions/kelly.js +16 -3
- backend/src/positions/positions.controller.js +14 -0
- backend/src/positions/positions.repository.js +13 -0
- backend/src/positions/positions.routes.js +11 -0
- backend/src/positions/positions.service.js +20 -0
- backend/src/positions/positions.validators.js +12 -0
- backend/src/scheduler.js +13 -0
- backend/src/signals/aiPipeline.js +128 -10
- backend/src/signals/finnhub.client.js +16 -0
- backend/src/signals/signals.controller.js +14 -0
- backend/src/signals/signals.repository.js +13 -0
- backend/src/signals/signals.routes.js +9 -0
- backend/src/signals/signals.service.js +18 -0
- backend/src/socket/broadcaster.js +15 -0
- backend/src/utils/apiResponse.js +14 -0
- backend/src/utils/httpClient.js +15 -0
- backend/src/utils/logger.js +11 -0
- backend/src/utils/prisma.js +13 -0
- backend/src/watchlist/watchlist.controller.js +14 -0
- backend/src/watchlist/watchlist.repository.js +15 -0
- backend/src/watchlist/watchlist.routes.js +11 -0
- backend/src/watchlist/watchlist.service.js +15 -0
- 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 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 18 |
-
- **Frontend:** Vanilla JS + Vite
|
| 19 |
-
- **IA:** HuggingFace
|
| 20 |
- **Datos:** Polymarket Gamma API + Finnhub REST
|
| 21 |
- **Deploy:** HuggingFace Spaces (Docker, puerto 7860)
|
| 22 |
|
| 23 |
## Estado del proyecto
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
## Estructura
|
| 28 |
|
| 29 |
```
|
| 30 |
polysignal/
|
| 31 |
-
βββ backend/
|
| 32 |
β βββ package.json
|
| 33 |
β βββ prisma/
|
| 34 |
-
β β
|
|
|
|
|
|
|
| 35 |
β βββ src/
|
| 36 |
-
β βββ index.js
|
| 37 |
-
β βββ
|
| 38 |
-
β βββ
|
| 39 |
-
β βββ
|
| 40 |
-
β βββ
|
| 41 |
-
β
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
β
|
| 43 |
-
βββ frontend/
|
| 44 |
-
β βββ index.html
|
| 45 |
-
β βββ package.json
|
| 46 |
-
β βββ vite.config.js
|
| 47 |
β βββ src/
|
| 48 |
-
β βββ main.js
|
| 49 |
-
β βββ app.js
|
| 50 |
-
β βββ api.js
|
| 51 |
-
β βββ charts.js
|
| 52 |
-
β βββ map.js
|
| 53 |
-
β βββ simulator.js
|
| 54 |
-
β βββ style.css
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
β
|
| 56 |
-
βββ package.json
|
| 57 |
-
βββ docker-compose.yml
|
| 58 |
-
βββ Dockerfile
|
| 59 |
-
βββ .env.example
|
|
|
|
| 60 |
βββ README.md
|
| 61 |
```
|
| 62 |
|
|
@@ -71,13 +143,11 @@ polysignal/
|
|
| 71 |
# 1. Instalar dependencias (root + todos los workspaces)
|
| 72 |
npm install
|
| 73 |
|
| 74 |
-
# 2.
|
| 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.
|
| 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
|
| 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
|
| 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 |
-
|
| 146 |
-
npm run
|
|
|
|
|
|
|
| 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
|
| 3 |
-
*
|
| 4 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
// ββ
|
| 47 |
|
| 48 |
-
async function
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 (
|
| 142 |
headlines = await filterWithFinBERT(relevant);
|
| 143 |
} else {
|
| 144 |
-
headlines =
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
try {
|
| 155 |
-
result = await
|
| 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({
|