---
id: "RF-ML-005"
title: "Notificaciones y Alertas de Senales"
type: "Requirement"
status: "Done"
priority: "Alta"
epic: "OQI-006"
project: "trading-platform"
version: "1.0.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---

# RF-ML-005: Notificaciones y Alertas de Señales

**Versión:** 1.0.0
**Fecha:** 2025-12-05
**Épica:** OQI-006 - Señales ML y Predicciones
**Prioridad:** P2
**Story Points:** 7

---

## Descripción

El sistema debe enviar notificaciones en tiempo real a los usuarios cuando se generen nuevas señales de trading de alta prioridad, cuando se alcancen niveles de TP/SL, o cuando ocurran eventos importantes en el modelo ML.

---

## Requisitos Funcionales

### RF-ML-005.1: Tipos de Notificaciones

El sistema debe soportar los siguientes tipos de notificaciones:

| Tipo | Prioridad | Descripción |
|------|-----------|-------------|
| **NEW_SIGNAL** | Alta | Nueva señal BUY/SELL generada |
| **TP_HIT** | Media | Take Profit alcanzado |
| **SL_HIT** | Alta | Stop Loss alcanzado |
| **SIGNAL_EXPIRED** | Baja | Señal expiró sin ejecutarse |
| **MODEL_RETRAINED** | Media | Modelo re-entrenado con nuevas métricas |
| **LOW_CONFIDENCE** | Baja | Confianza del modelo cayó bajo umbral |

### RF-ML-005.2: Canales de Notificación

El sistema debe soportar múltiples canales:

**In-App (Prioridad 1):**
- Notificaciones en la plataforma web
- Badge counter en navbar
- Panel de notificaciones con historial

**Push Notifications (Prioridad 2):**
- Web Push API para navegadores
- Notificaciones desktop

**Email (Prioridad 3):**
- Resumen diario de señales
- Alertas críticas (SL hit)

**Webhook (Prioridad 4):**
- Integración con Telegram/Discord
- Webhooks personalizados

### RF-ML-005.3: Configuración de Alertas por Usuario

Cada usuario debe poder configurar:

```typescript
interface AlertPreferences {
  user_id: string;

  // Canales activos
  channels: {
    in_app: boolean;
    push: boolean;
    email: boolean;
    webhook?: string;  // URL del webhook
  };

  // Filtros de señales
  filters: {
    min_priority: 'HIGH' | 'MEDIUM' | 'LOW';
    symbols: string[];              // Vacío = todos
    horizons: string[];             // Vacío = todos
    actions: ('BUY' | 'SELL')[];   // Vacío = ambos
    min_confidence: number;         // 0-1
    min_score: number;              // 0-10
  };

  // Configuración de horarios
  quiet_hours?: {
    enabled: boolean;
    start_hour: number;  // 0-23
    end_hour: number;
    timezone: string;
  };

  // Rate limiting
  max_notifications_per_hour?: number;
}
```

### RF-ML-005.4: Contenido de Notificaciones

**NEW_SIGNAL - Nueva Señal:**
```json
{
  "type": "NEW_SIGNAL",
  "timestamp": "2025-12-05T19:00:00.000Z",
  "priority": "HIGH",
  "title": "Nueva señal BUY para BTCUSDT",
  "message": "Señal de compra con confianza 75% - TP: $89,900 | SL: $89,150",
  "data": {
    "signal_id": "550e8400-e29b-41d4-a716-446655440000",
    "symbol": "BTCUSDT",
    "action": "BUY",
    "horizon": "scalping",
    "entry_price": 89400.00,
    "tp1": 89650.00,
    "tp2": 89775.00,
    "tp3": 89900.00,
    "stop_loss": 89150.00,
    "confidence": 0.75,
    "score": 8.5
  },
  "actions": [
    {
      "label": "Ver Detalles",
      "url": "/signals/550e8400-e29b-41d4-a716-446655440000"
    },
    {
      "label": "Ver Chart",
      "url": "/trading/BTCUSDT"
    }
  ]
}
```

**TP_HIT - Take Profit Alcanzado:**
```json
{
  "type": "TP_HIT",
  "timestamp": "2025-12-05T19:25:00.000Z",
  "priority": "MEDIUM",
  "title": "TP1 alcanzado - BTCUSDT",
  "message": "Tu señal alcanzó TP1 ($89,650) con ganancia de +0.28%",
  "data": {
    "signal_id": "550e8400-e29b-41d4-a716-446655440000",
    "symbol": "BTCUSDT",
    "tp_level": "TP1",
    "tp_price": 89650.00,
    "entry_price": 89400.00,
    "current_price": 89655.00,
    "gain_pct": 0.28
  }
}
```

**SL_HIT - Stop Loss Alcanzado:**
```json
{
  "type": "SL_HIT",
  "timestamp": "2025-12-05T19:15:00.000Z",
  "priority": "HIGH",
  "title": "Stop Loss activado - BTCUSDT",
  "message": "Tu señal alcanzó SL ($89,150) con pérdida de -0.28%",
  "data": {
    "signal_id": "550e8400-e29b-41d4-a716-446655440000",
    "symbol": "BTCUSDT",
    "stop_loss": 89150.00,
    "entry_price": 89400.00,
    "current_price": 89145.00,
    "loss_pct": -0.28
  }
}
```

### RF-ML-005.5: Notificaciones In-App

El sistema debe:
- Mostrar badge counter en el navbar con número de notificaciones no leídas
- Panel de notificaciones accesible con click en icono campana
- Listar notificaciones ordenadas por fecha (más recientes primero)
- Marcar como leída al hacer click
- Opción "Marcar todas como leídas"
- Eliminar notificaciones antiguas (>30 días)

**UI del Panel:**
```
┌─────────────────────────────────────────┐
│  Notificaciones              [x]        │
├─────────────────────────────────────────┤
│  [•] Nueva señal BUY - BTCUSDT          │
│      Confianza 75% | Hace 5 min         │
│                                          │
│  [•] TP1 alcanzado - ETHUSDT            │
│      Ganancia +0.5% | Hace 15 min       │
│                                          │
│  [ ] Modelo re-entrenado                │
│      Nuevo MAE: 0.0012 | Hace 2h        │
│                                          │
│  [ ] Señal expirada - BTCUSDT           │
│      No ejecutada | Hace 5h             │
├─────────────────────────────────────────┤
│  [Marcar todas como leídas]             │
│  [Ver todas las notificaciones]         │
└─────────────────────────────────────────┘
```

### RF-ML-005.6: Rate Limiting y Anti-Spam

El sistema debe:
- Limitar a máximo 20 notificaciones por hora por usuario (configurable)
- Agrupar notificaciones similares (ej: múltiples señales del mismo símbolo)
- Respetar horarios de silencio (quiet hours) configurados por el usuario
- No enviar notificaciones duplicadas

**Agrupación de Notificaciones:**
```json
{
  "type": "NEW_SIGNAL_BATCH",
  "timestamp": "2025-12-05T19:00:00.000Z",
  "title": "3 nuevas señales generadas",
  "message": "BTCUSDT (BUY), ETHUSDT (BUY), BNBUSDT (SELL)",
  "data": {
    "count": 3,
    "signals": [
      { "symbol": "BTCUSDT", "action": "BUY" },
      { "symbol": "ETHUSDT", "action": "BUY" },
      { "symbol": "BNBUSDT", "action": "SELL" }
    ]
  }
}
```

---

## Datos de Entrada

### Crear/Actualizar Preferencias

| Campo | Tipo | Descripción | Requerido |
|-------|------|-------------|-----------|
| channels | object | Canales activos | Sí |
| filters | object | Filtros de señales | Sí |
| quiet_hours | object | Horarios de silencio | No |
| max_notifications_per_hour | number | Rate limit | No |

---

## Datos de Salida

### Obtener Notificaciones

```typescript
interface Notification {
  id: string;
  user_id: string;
  type: NotificationType;
  priority: 'HIGH' | 'MEDIUM' | 'LOW';
  title: string;
  message: string;
  data: any;
  actions?: NotificationAction[];
  read: boolean;
  created_at: string;
}

interface NotificationList {
  notifications: Notification[];
  unread_count: number;
  total_count: number;
  page: number;
  page_size: number;
}
```

---

## Reglas de Negocio

1. **Notificaciones Críticas:** SL_HIT siempre se envía, ignorando quiet hours
2. **Deduplicación:** Una señal genera solo 1 notificación NEW_SIGNAL
3. **Expiración:** Notificaciones mayores a 30 días se eliminan automáticamente
4. **Batch Processing:** Señales generadas en batch (cada 5 min) se agrupan
5. **Prioridad de Canales:** In-app > Push > Email > Webhook
6. **Límite de Webhook:** Máximo 3 webhooks por usuario

---

## Criterios de Aceptación

```gherkin
Escenario: Recibir notificación de nueva señal HIGH
  DADO que el usuario tiene alertas activas
  Y min_priority = "HIGH"
  CUANDO se genera una señal BUY de prioridad HIGH
  ENTONCES recibe notificación in-app
  Y el badge counter incrementa en 1
  Y la notificación incluye detalles de la señal

Escenario: Respetar quiet hours
  DADO que el usuario configuró quiet_hours de 22:00 a 8:00
  Y son las 23:30
  CUANDO se genera una señal MEDIUM
  ENTONCES NO recibe notificación
  Y la notificación se almacena para verla después

Escenario: Notificación de Stop Loss (crítica)
  DADO que el usuario configuró quiet_hours
  Y son las 2:00 AM
  CUANDO una señal alcanza el Stop Loss
  ENTONCES recibe notificación SL_HIT (ignora quiet hours)
  Y la prioridad es HIGH

Escenario: Marcar notificación como leída
  DADO que el usuario tiene 5 notificaciones no leídas
  CUANDO hace click en una notificación
  ENTONCES se marca como leída (read = true)
  Y el unread_count disminuye a 4
  Y navega a la página de detalles
```

---

## Dependencias

### Técnicas:
- **WebSocket:** Para notificaciones in-app en tiempo real
- **Web Push API:** Para notificaciones push de navegador
- **Email Service (SendGrid/AWS SES):** Para emails
- **PostgreSQL:** Almacenamiento de notificaciones
- **Redis:** Caché de preferencias y rate limiting

### Funcionales:
- **RF-ML-002:** Generación de señales (trigger de notificaciones)
- **RF-AUTH-005:** Sessions (identificar usuario activo)

---

## Notas Técnicas

### Arquitectura de Notificaciones

```python
# apps/ml-services/src/notifications/notification_service.py

class NotificationService:
    """
    Servicio de notificaciones multi-canal
    """

    async def send_notification(
        self,
        user_id: str,
        notification: Notification
    ):
        # 1. Obtener preferencias del usuario
        prefs = await self.get_user_preferences(user_id)

        # 2. Filtrar según preferencias
        if not self.should_send(notification, prefs):
            return

        # 3. Verificar quiet hours
        if self.is_quiet_hours(prefs) and notification.priority != 'HIGH':
            await self.queue_for_later(user_id, notification)
            return

        # 4. Verificar rate limit
        if await self.is_rate_limited(user_id):
            await self.queue_for_later(user_id, notification)
            return

        # 5. Enviar a canales activos
        tasks = []
        if prefs.channels.in_app:
            tasks.append(self.send_in_app(user_id, notification))
        if prefs.channels.push:
            tasks.append(self.send_push(user_id, notification))
        if prefs.channels.email:
            tasks.append(self.send_email(user_id, notification))
        if prefs.channels.webhook:
            tasks.append(self.send_webhook(prefs.channels.webhook, notification))

        await asyncio.gather(*tasks)

        # 6. Guardar en base de datos
        await self.save_notification(user_id, notification)
```

### Base de Datos - Tabla `notifications`

```sql
CREATE TABLE notifications (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES users(id),
  type VARCHAR(50) NOT NULL,
  priority VARCHAR(10) NOT NULL,
  title VARCHAR(200) NOT NULL,
  message TEXT NOT NULL,
  data JSONB,
  read BOOLEAN DEFAULT FALSE,
  created_at TIMESTAMP DEFAULT NOW(),

  INDEX idx_user_id (user_id),
  INDEX idx_created_at (created_at),
  INDEX idx_read (read)
);

CREATE TABLE user_alert_preferences (
  user_id UUID PRIMARY KEY REFERENCES users(id),
  channels JSONB NOT NULL,
  filters JSONB NOT NULL,
  quiet_hours JSONB,
  max_notifications_per_hour INTEGER DEFAULT 20,
  updated_at TIMESTAMP DEFAULT NOW()
);
```

### WebSocket Event

```typescript
// Cliente se suscribe a notificaciones
socket.on('subscribe_notifications', (userId) => {
  // Join room personal
  socket.join(`notifications:${userId}`);
});

// Servidor envía notificación
io.to(`notifications:${userId}`).emit('new_notification', notification);
```

---

## Referencias

- [Web Push API](https://developer.mozilla.org/en-US/docs/Web/API/Push_API)
- [SendGrid Email API](https://docs.sendgrid.com/api-reference/mail-send/mail-send)
- [Socket.io Rooms](https://socket.io/docs/v4/rooms/)

---

**Creado por:** Requirements-Analyst
**Fecha:** 2025-12-05
**Última actualización:** 2025-12-05
