---
id: "INT-DATA-003"
title: "Batch de Actualizacion de Activos con Priorizacion"
type: "Integration Specification"
project: "trading-platform"
version: "1.0.0"
status: "Planificado"
priority: "Alta"
created_date: "2026-01-04"
updated_date: "2026-01-04"
author: "Orquestador Agent"
---

# INT-DATA-003: Proceso Batch de Actualizacion de Activos con Priorizacion

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | INT-DATA-003 |
| **Modulo** | Data Service |
| **Tipo** | Especificacion de Integracion |
| **Version** | 1.0.0 |
| **Estado** | Planificado |
| **Fecha creacion** | 2026-01-04 |
| **Ultima actualizacion** | 2026-01-04 |
| **Autor** | Orquestador Agent |

---

## 1. Resumen Ejecutivo

Este documento especifica la implementacion de un **proceso batch de actualizacion de datos de activos financieros** desde la API de Polygon.io/Massive.com hacia la base de datos PostgreSQL del proyecto trading-platform.

### Caracteristicas Principales:
- **Ejecucion cada 5 minutos** - Proceso batch programado
- **Priorizacion de activos** - Oro (XAU), EURUSD y Bitcoin se actualizan primero
- **Rate limiting** - 5 llamadas API por minuto (cuenta gratuita)
- **Sistema de colas** - Activos no prioritarios se encolan para actualizacion diferida

---

## 2. Contexto del Requerimiento

### 2.1 Situacion Actual

El proyecto ya cuenta con:
- `PolygonClient` en `apps/data-service/src/providers/polygon_client.py`
- `DataSyncScheduler` en `apps/data-service/src/services/scheduler.py`
- Tabla `trading.symbols` para catalogo de activos
- Infraestructura de sync incremental

### 2.2 Necesidad

Se requiere:
1. Actualizar datos de activos respetando el rate limit de 5 calls/min
2. Priorizar activos criticos (XAU, EURUSD, BTCUSD) en cada ciclo
3. Encolar activos secundarios para actualizacion gradual
4. Mantener datos frescos para el ML Engine

### 2.3 Restricciones

| Restriccion | Valor | Nota |
|-------------|-------|------|
| Rate Limit API | 5 calls/min | Cuenta gratuita |
| Intervalo Batch | 5 minutos | Configurable |
| Activos Prioritarios | 3 | XAU, EURUSD, BTCUSD |
| Tiempo Maximo Ciclo | 60 segundos | 5 calls * 12s spacing |

---

## 3. Arquitectura de la Solucion

### 3.1 Diagrama de Componentes

```
┌─────────────────────────────────────────────────────────────────────────────┐
│                     BATCH ASSET UPDATE SYSTEM                                │
│                                                                              │
│  ┌────────────────────────────────────────────────────────────────────────┐ │
│  │                    SCHEDULER LAYER (APScheduler)                       │ │
│  │                                                                        │ │
│  │   ┌─────────────────────┐     ┌─────────────────────────────────────┐ │ │
│  │   │  PriorityBatchJob   │     │       QueueProcessorJob             │ │ │
│  │   │  Every 5 minutes    │     │       Continuous                    │ │ │
│  │   │                     │     │                                     │ │ │
│  │   │  1. XAU/USD         │     │  Process queued assets              │ │ │
│  │   │  2. EUR/USD         │     │  respecting rate limit              │ │ │
│  │   │  3. BTC/USD         │     │                                     │ │ │
│  │   └──────────┬──────────┘     └──────────────┬──────────────────────┘ │ │
│  └──────────────┼───────────────────────────────┼────────────────────────┘ │
│                 │                               │                          │
│  ┌──────────────▼───────────────────────────────▼────────────────────────┐ │
│  │                        SERVICE LAYER                                   │ │
│  │                                                                        │ │
│  │   ┌─────────────────────┐     ┌─────────────────────────────────────┐ │ │
│  │   │ PriorityQueueService│     │      AssetUpdateService             │ │ │
│  │   │                     │     │                                     │ │ │
│  │   │  • Priority Queue   │     │  • Fetch from API                   │ │ │
│  │   │  • FIFO for others  │     │  • Update database                  │ │ │
│  │   │  • Deduplication    │     │  • Emit events                      │ │ │
│  │   └──────────┬──────────┘     └──────────────┬──────────────────────┘ │ │
│  └──────────────┼───────────────────────────────┼────────────────────────┘ │
│                 │                               │                          │
│  ┌──────────────▼───────────────────────────────▼────────────────────────┐ │
│  │                      PROVIDER LAYER                                    │ │
│  │                                                                        │ │
│  │   ┌─────────────────────┐     ┌─────────────────────────────────────┐ │ │
│  │   │   RateLimiter       │────▶│      PolygonClient                  │ │ │
│  │   │   5 req/min         │     │      (existing)                     │ │ │
│  │   └─────────────────────┘     └─────────────────────────────────────┘ │ │
│  └───────────────────────────────────────────────────────────────────────┘ │
│                                    │                                        │
└────────────────────────────────────┼────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           EXTERNAL SYSTEMS                                   │
│                                                                              │
│  ┌────────────────────────┐     ┌───────────────────────────────────────┐  │
│  │   Polygon.io API       │     │          PostgreSQL                    │  │
│  │   api.polygon.io       │     │          trading.symbols               │  │
│  │                        │     │          market_data.ohlcv_5m          │  │
│  │   Rate: 5 calls/min    │     │          data_sources.sync_status      │  │
│  └────────────────────────┘     └───────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────────────────────┘
```

### 3.2 Flujo de Ejecucion

```
                    Cada 5 minutos
                         │
                         ▼
              ┌──────────────────────┐
              │  Batch Job Trigger   │
              └──────────┬───────────┘
                         │
           ┌─────────────┴─────────────┐
           │                           │
           ▼                           ▼
┌───────────────────────┐   ┌───────────────────────┐
│  Priority Assets      │   │  Enqueue Non-Priority │
│  XAU, EURUSD, BTCUSD  │   │  to Update Queue      │
└───────────┬───────────┘   └───────────┬───────────┘
            │                           │
            ▼                           ▼
┌───────────────────────┐   ┌───────────────────────┐
│  Rate Limited Fetch   │   │  Background Processor │
│  (3 calls, 12s each)  │   │  (2 calls remaining)  │
└───────────┬───────────┘   └───────────┬───────────┘
            │                           │
            ▼                           ▼
┌───────────────────────┐   ┌───────────────────────┐
│  Update Database      │   │  Process Queue Items  │
│  trading.symbols      │   │  (gradual sync)       │
└───────────┬───────────┘   └───────────┬───────────┘
            │                           │
            └─────────────┬─────────────┘
                          │
                          ▼
              ┌───────────────────────┐
              │  Emit Update Events   │
              │  via Redis Pub/Sub    │
              └───────────────────────┘
```

---

## 4. Especificacion Tecnica

### 4.1 Configuracion de Activos Prioritarios

```python
# config/priority_assets.py

PRIORITY_ASSETS = [
    {
        "symbol": "XAUUSD",
        "polygon_ticker": "C:XAUUSD",
        "asset_type": "forex",  # Oro como commodity via forex
        "name": "Gold Spot",
        "priority": 1,
        "update_frequency_seconds": 300  # 5 min
    },
    {
        "symbol": "EURUSD",
        "polygon_ticker": "C:EURUSD",
        "asset_type": "forex",
        "name": "Euro/US Dollar",
        "priority": 2,
        "update_frequency_seconds": 300
    },
    {
        "symbol": "BTCUSD",
        "polygon_ticker": "X:BTCUSD",
        "asset_type": "crypto",
        "name": "Bitcoin/US Dollar",
        "priority": 3,
        "update_frequency_seconds": 300
    }
]

# Activos secundarios (se encolan)
SECONDARY_ASSETS = [
    {"symbol": "ETHUSDT", "polygon_ticker": "X:ETHUSDT", "asset_type": "crypto"},
    {"symbol": "GBPUSD", "polygon_ticker": "C:GBPUSD", "asset_type": "forex"},
    {"symbol": "USDJPY", "polygon_ticker": "C:USDJPY", "asset_type": "forex"},
    {"symbol": "XAGUSD", "polygon_ticker": "C:XAGUSD", "asset_type": "forex"},  # Plata
    # ... mas activos
]
```

### 4.2 Servicio de Cola de Prioridad

```python
# services/priority_queue_service.py

import asyncio
from datetime import datetime
from typing import Optional, List, Dict
from dataclasses import dataclass, field
from enum import Enum
import heapq
import logging

logger = logging.getLogger(__name__)


class AssetPriority(Enum):
    CRITICAL = 1   # XAU, EURUSD, BTC
    HIGH = 2       # Major forex pairs
    MEDIUM = 3     # Other crypto
    LOW = 4        # Indices, commodities


@dataclass(order=True)
class QueuedAsset:
    """Asset in the update queue"""
    priority: int
    enqueued_at: datetime = field(compare=False)
    symbol: str = field(compare=False)
    polygon_ticker: str = field(compare=False)
    asset_type: str = field(compare=False)
    retry_count: int = field(default=0, compare=False)


class PriorityQueueService:
    """
    Manages priority queue for asset updates.

    Priority levels:
    1 - Critical (XAU, EURUSD, BTC) - Always updated first
    2 - High - Updated when slots available
    3 - Medium - Gradual updates
    4 - Low - Best effort
    """

    def __init__(self, max_queue_size: int = 1000):
        self._queue: List[QueuedAsset] = []
        self._in_queue: set = set()
        self.max_queue_size = max_queue_size
        self._lock = asyncio.Lock()

    async def enqueue(
        self,
        symbol: str,
        polygon_ticker: str,
        asset_type: str,
        priority: AssetPriority = AssetPriority.MEDIUM
    ) -> bool:
        """Add asset to update queue if not already present"""
        async with self._lock:
            if symbol in self._in_queue:
                logger.debug(f"Asset {symbol} already in queue, skipping")
                return False

            if len(self._queue) >= self.max_queue_size:
                logger.warning(f"Queue full, dropping {symbol}")
                return False

            item = QueuedAsset(
                priority=priority.value,
                enqueued_at=datetime.utcnow(),
                symbol=symbol,
                polygon_ticker=polygon_ticker,
                asset_type=asset_type
            )

            heapq.heappush(self._queue, item)
            self._in_queue.add(symbol)

            logger.debug(f"Enqueued {symbol} with priority {priority.name}")
            return True

    async def dequeue(self) -> Optional[QueuedAsset]:
        """Get next asset to update (highest priority first)"""
        async with self._lock:
            if not self._queue:
                return None

            item = heapq.heappop(self._queue)
            self._in_queue.discard(item.symbol)
            return item

    async def peek(self) -> Optional[QueuedAsset]:
        """View next item without removing"""
        async with self._lock:
            return self._queue[0] if self._queue else None

    @property
    def size(self) -> int:
        return len(self._queue)

    @property
    def is_empty(self) -> bool:
        return len(self._queue) == 0

    async def get_queue_stats(self) -> Dict:
        """Get queue statistics"""
        async with self._lock:
            priority_counts = {p.name: 0 for p in AssetPriority}
            for item in self._queue:
                priority_name = AssetPriority(item.priority).name
                priority_counts[priority_name] += 1

            return {
                "total_items": len(self._queue),
                "by_priority": priority_counts,
                "oldest_item_age_seconds": (
                    (datetime.utcnow() - self._queue[0].enqueued_at).total_seconds()
                    if self._queue else 0
                )
            }
```

### 4.3 Servicio de Actualizacion de Activos

```python
# services/asset_update_service.py

import asyncio
from datetime import datetime, timedelta
from typing import Optional, Dict, List, Any
import logging

from providers.polygon_client import PolygonClient, AssetType, TickerSnapshot
from config.priority_assets import PRIORITY_ASSETS, SECONDARY_ASSETS
from services.priority_queue_service import PriorityQueueService, AssetPriority

logger = logging.getLogger(__name__)


class AssetUpdateService:
    """
    Service for updating asset data from Polygon API.

    Handles:
    - Rate limiting (5 calls/min)
    - Priority-based updates
    - Database synchronization
    - Error handling and retries
    """

    def __init__(
        self,
        polygon_client: PolygonClient,
        db_pool,
        redis_client,
        priority_queue: PriorityQueueService
    ):
        self.polygon = polygon_client
        self.db = db_pool
        self.redis = redis_client
        self.queue = priority_queue

        # Rate limiting state
        self._calls_this_minute = 0
        self._minute_start = datetime.utcnow()
        self._rate_limit = 5

    async def _wait_for_rate_limit(self):
        """Wait if rate limit reached"""
        now = datetime.utcnow()

        # Reset counter each minute
        if (now - self._minute_start).total_seconds() >= 60:
            self._calls_this_minute = 0
            self._minute_start = now

        # Wait if limit reached
        if self._calls_this_minute >= self._rate_limit:
            wait_time = 60 - (now - self._minute_start).total_seconds()
            if wait_time > 0:
                logger.info(f"Rate limit reached, waiting {wait_time:.1f}s")
                await asyncio.sleep(wait_time)
                self._calls_this_minute = 0
                self._minute_start = datetime.utcnow()

        self._calls_this_minute += 1

    async def update_priority_assets(self) -> Dict[str, Any]:
        """
        Update all priority assets (XAU, EURUSD, BTC).
        Uses 3 of 5 available API calls.
        """
        results = {
            "updated": [],
            "failed": [],
            "api_calls_used": 0
        }

        for asset in PRIORITY_ASSETS:
            try:
                await self._wait_for_rate_limit()

                # Fetch snapshot from Polygon
                snapshot = await self._fetch_asset_snapshot(
                    asset["polygon_ticker"],
                    asset["asset_type"]
                )

                if snapshot:
                    # Update database
                    await self._update_asset_in_db(asset, snapshot)

                    # Publish update event
                    await self._publish_update_event(asset["symbol"], snapshot)

                    results["updated"].append(asset["symbol"])
                else:
                    results["failed"].append({
                        "symbol": asset["symbol"],
                        "error": "No data returned"
                    })

                results["api_calls_used"] += 1

            except Exception as e:
                logger.error(f"Error updating {asset['symbol']}: {e}")
                results["failed"].append({
                    "symbol": asset["symbol"],
                    "error": str(e)
                })

        return results

    async def _fetch_asset_snapshot(
        self,
        polygon_ticker: str,
        asset_type: str
    ) -> Optional[TickerSnapshot]:
        """Fetch current snapshot from Polygon API"""
        try:
            if asset_type == "forex":
                return await self.polygon.get_snapshot_forex(polygon_ticker)
            elif asset_type == "crypto":
                return await self.polygon.get_snapshot_crypto(polygon_ticker)
            else:
                logger.warning(f"Unknown asset type: {asset_type}")
                return None
        except Exception as e:
            logger.error(f"Error fetching {polygon_ticker}: {e}")
            return None

    async def _update_asset_in_db(
        self,
        asset: Dict,
        snapshot: TickerSnapshot
    ):
        """Update asset data in PostgreSQL"""
        async with self.db.acquire() as conn:
            # Update trading.symbols with latest price info
            await conn.execute("""
                UPDATE trading.symbols
                SET
                    updated_at = NOW(),
                    -- Store latest prices in a JSONB column if exists
                    metadata = jsonb_set(
                        COALESCE(metadata, '{}'),
                        '{last_update}',
                        $2::jsonb
                    )
                WHERE symbol = $1
            """, asset["symbol"], {
                "bid": snapshot.bid,
                "ask": snapshot.ask,
                "last_price": snapshot.last_price,
                "daily_open": snapshot.daily_open,
                "daily_high": snapshot.daily_high,
                "daily_low": snapshot.daily_low,
                "daily_close": snapshot.daily_close,
                "timestamp": snapshot.timestamp.isoformat()
            })

            # Update sync status
            await conn.execute("""
                INSERT INTO data_sources.data_sync_status
                (ticker_id, provider_id, last_sync_timestamp, sync_status, updated_at)
                SELECT
                    s.id,
                    (SELECT id FROM data_sources.api_providers WHERE code = 'polygon'),
                    NOW(),
                    'success',
                    NOW()
                FROM trading.symbols s
                WHERE s.symbol = $1
                ON CONFLICT (ticker_id, provider_id)
                DO UPDATE SET
                    last_sync_timestamp = NOW(),
                    sync_status = 'success',
                    updated_at = NOW()
            """, asset["symbol"])

    async def _publish_update_event(
        self,
        symbol: str,
        snapshot: TickerSnapshot
    ):
        """Publish update event via Redis for real-time consumers"""
        channel = f"asset:update:{symbol}"
        message = {
            "symbol": symbol,
            "bid": snapshot.bid,
            "ask": snapshot.ask,
            "last_price": snapshot.last_price,
            "timestamp": snapshot.timestamp.isoformat()
        }

        await self.redis.publish(channel, str(message))

    async def process_queued_asset(self) -> Optional[Dict]:
        """
        Process next asset from queue.
        Uses remaining API calls (2 of 5 per cycle).
        """
        item = await self.queue.dequeue()
        if not item:
            return None

        try:
            await self._wait_for_rate_limit()

            snapshot = await self._fetch_asset_snapshot(
                item.polygon_ticker,
                item.asset_type
            )

            if snapshot:
                asset = {
                    "symbol": item.symbol,
                    "polygon_ticker": item.polygon_ticker,
                    "asset_type": item.asset_type
                }
                await self._update_asset_in_db(asset, snapshot)
                await self._publish_update_event(item.symbol, snapshot)

                return {"symbol": item.symbol, "status": "success"}
            else:
                # Re-enqueue with lower priority on failure
                if item.retry_count < 3:
                    item.retry_count += 1
                    await self.queue.enqueue(
                        item.symbol,
                        item.polygon_ticker,
                        item.asset_type,
                        AssetPriority.LOW
                    )
                return {"symbol": item.symbol, "status": "failed", "requeued": True}

        except Exception as e:
            logger.error(f"Error processing queued asset {item.symbol}: {e}")
            return {"symbol": item.symbol, "status": "error", "error": str(e)}

    async def enqueue_secondary_assets(self):
        """Enqueue all secondary assets for gradual update"""
        for asset in SECONDARY_ASSETS:
            await self.queue.enqueue(
                symbol=asset["symbol"],
                polygon_ticker=asset["polygon_ticker"],
                asset_type=asset["asset_type"],
                priority=AssetPriority.MEDIUM
            )

        logger.info(f"Enqueued {len(SECONDARY_ASSETS)} secondary assets")
```

### 4.4 Job de Batch Programado

```python
# services/batch_scheduler.py

from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.interval import IntervalTrigger
from datetime import datetime
import logging

logger = logging.getLogger(__name__)


class AssetBatchScheduler:
    """
    Scheduler for batch asset updates.

    Schedule:
    - Every 5 minutes: Update priority assets (XAU, EURUSD, BTC)
    - Continuous: Process queued secondary assets
    """

    def __init__(
        self,
        asset_update_service,
        batch_interval_minutes: int = 5
    ):
        self.update_service = asset_update_service
        self.batch_interval = batch_interval_minutes
        self.scheduler = AsyncIOScheduler()
        self._is_running = False

    async def start(self):
        """Start the batch scheduler"""
        if self._is_running:
            logger.warning("Scheduler already running")
            return

        logger.info("Starting Asset Batch Scheduler")

        # Priority assets job - every 5 minutes
        self.scheduler.add_job(
            self._priority_batch_job,
            trigger=IntervalTrigger(minutes=self.batch_interval),
            id="priority_assets_batch",
            name="Update Priority Assets (XAU, EURUSD, BTC)",
            replace_existing=True,
            max_instances=1,
            next_run_time=datetime.now()  # Run immediately on start
        )

        # Queue processor job - every 15 seconds
        self.scheduler.add_job(
            self._queue_processor_job,
            trigger=IntervalTrigger(seconds=15),
            id="queue_processor",
            name="Process Queued Secondary Assets",
            replace_existing=True,
            max_instances=1
        )

        # Enqueue secondary assets job - every 30 minutes
        self.scheduler.add_job(
            self._enqueue_secondary_job,
            trigger=IntervalTrigger(minutes=30),
            id="enqueue_secondary",
            name="Enqueue Secondary Assets",
            replace_existing=True,
            max_instances=1
        )

        self.scheduler.start()
        self._is_running = True

        logger.info(f"Scheduler started with {len(self.scheduler.get_jobs())} jobs")

    async def _priority_batch_job(self):
        """Job: Update priority assets"""
        logger.info("=== Priority Batch Job Started ===")
        start_time = datetime.utcnow()

        try:
            result = await self.update_service.update_priority_assets()

            elapsed = (datetime.utcnow() - start_time).total_seconds()

            logger.info(
                f"Priority batch completed in {elapsed:.1f}s: "
                f"{len(result['updated'])} updated, "
                f"{len(result['failed'])} failed, "
                f"{result['api_calls_used']} API calls"
            )

            # Log details
            for symbol in result['updated']:
                logger.debug(f"  [OK] {symbol}")
            for fail in result['failed']:
                logger.warning(f"  [FAIL] {fail['symbol']}: {fail['error']}")

        except Exception as e:
            logger.error(f"Priority batch job failed: {e}", exc_info=True)

    async def _queue_processor_job(self):
        """Job: Process queued secondary assets"""
        # Only process if we have API calls available
        # Since priority uses 3, we have 2 remaining per minute
        try:
            for _ in range(2):  # Process up to 2 items per cycle
                result = await self.update_service.process_queued_asset()
                if not result:
                    break  # Queue empty

                if result['status'] == 'success':
                    logger.debug(f"Queue: Updated {result['symbol']}")
                else:
                    logger.debug(f"Queue: Failed {result['symbol']}")

        except Exception as e:
            logger.error(f"Queue processor job failed: {e}")

    async def _enqueue_secondary_job(self):
        """Job: Enqueue secondary assets for update"""
        try:
            await self.update_service.enqueue_secondary_assets()
        except Exception as e:
            logger.error(f"Enqueue secondary job failed: {e}")

    async def stop(self):
        """Stop the scheduler"""
        if self._is_running:
            self.scheduler.shutdown(wait=True)
            self._is_running = False
            logger.info("Scheduler stopped")

    def get_job_status(self) -> list:
        """Get status of all scheduled jobs"""
        return [
            {
                "id": job.id,
                "name": job.name,
                "next_run": job.next_run_time.isoformat() if job.next_run_time else None,
                "trigger": str(job.trigger)
            }
            for job in self.scheduler.get_jobs()
        ]
```

---

## 5. Configuracion

### 5.1 Variables de Entorno

```bash
# .env - Data Service Configuration

# Polygon.io API (Massive.com compatible)
POLYGON_API_KEY=f09bA2V7OG7bHn4HxIT6Xs45ujg_pRXk
POLYGON_BASE_URL=https://api.polygon.io
POLYGON_RATE_LIMIT=5
POLYGON_TIER=free

# Batch Configuration
BATCH_INTERVAL_MINUTES=5
PRIORITY_ASSETS_ENABLED=true
QUEUE_MAX_SIZE=1000

# Database
DB_HOST=localhost
DB_PORT=5432
DB_NAME=trading_data
DB_USER=trading_user
DB_PASSWORD=trading_dev_2025

# Redis (for queue and events)
REDIS_URL=redis://localhost:6379/0
```

### 5.2 Configuracion de Produccion

```yaml
# config/production.yml

batch:
  interval_minutes: 5
  priority_assets:
    - symbol: XAUUSD
      enabled: true
      priority: 1
    - symbol: EURUSD
      enabled: true
      priority: 2
    - symbol: BTCUSD
      enabled: true
      priority: 3

  rate_limiting:
    calls_per_minute: 5
    tier: free
    upgrade_url: https://polygon.io/pricing

queue:
  max_size: 1000
  retry_max_attempts: 3
  retry_delay_seconds: 60
```

---

## 6. Plan de Migracion a Cuenta de Pago

### 6.1 Cuando Migrar

Se recomienda migrar a cuenta de pago cuando:
- Se requieran datos en tiempo real (< 15 min delay)
- Se necesiten mas de 5 activos actualizados por minuto
- Se requiera soporte WebSocket para streaming

### 6.2 Planes Disponibles

| Plan | Rate Limit | Precio | Recomendado Para |
|------|------------|--------|------------------|
| Free | 5/min | $0 | Desarrollo, testing |
| Starter | Unlimited | $47/mo | Produccion basica |
| Developer | Unlimited | $99/mo | Produccion avanzada |
| Advanced | Unlimited | $199/mo | Tiempo real |

### 6.3 Cambios en Codigo para Plan de Pago

```python
# config/polygon_config.py

POLYGON_CONFIGS = {
    "free": {
        "rate_limit": 5,
        "delay_minutes": 15,
        "websocket_enabled": False
    },
    "starter": {
        "rate_limit": None,  # Unlimited
        "delay_minutes": 0,
        "websocket_enabled": False
    },
    "developer": {
        "rate_limit": None,
        "delay_minutes": 0,
        "websocket_enabled": True
    }
}
```

---

## 7. Testing

### 7.1 Tests Unitarios

```python
# tests/test_priority_queue.py

import pytest
from services.priority_queue_service import PriorityQueueService, AssetPriority

@pytest.mark.asyncio
async def test_priority_ordering():
    queue = PriorityQueueService()

    await queue.enqueue("ETH", "X:ETH", "crypto", AssetPriority.LOW)
    await queue.enqueue("XAU", "C:XAU", "forex", AssetPriority.CRITICAL)
    await queue.enqueue("GBP", "C:GBP", "forex", AssetPriority.MEDIUM)

    # Should dequeue by priority
    item = await queue.dequeue()
    assert item.symbol == "XAU"

    item = await queue.dequeue()
    assert item.symbol == "GBP"

    item = await queue.dequeue()
    assert item.symbol == "ETH"

@pytest.mark.asyncio
async def test_deduplication():
    queue = PriorityQueueService()

    result1 = await queue.enqueue("XAU", "C:XAU", "forex")
    result2 = await queue.enqueue("XAU", "C:XAU", "forex")

    assert result1 is True
    assert result2 is False
    assert queue.size == 1
```

### 7.2 Tests de Integracion

```python
# tests/test_batch_integration.py

import pytest
from unittest.mock import AsyncMock, patch

@pytest.mark.asyncio
async def test_priority_batch_respects_rate_limit():
    """Verify batch job uses max 3 API calls for priority assets"""
    with patch('providers.polygon_client.PolygonClient') as mock_client:
        mock_client.get_snapshot_forex = AsyncMock()
        mock_client.get_snapshot_crypto = AsyncMock()

        service = AssetUpdateService(mock_client, None, None, None)
        result = await service.update_priority_assets()

        # Should only make 3 calls (XAU, EURUSD, BTC)
        assert result['api_calls_used'] <= 3
```

---

## 8. Monitoreo

### 8.1 Metricas a Rastrear

| Metrica | Descripcion | Alerta |
|---------|-------------|--------|
| `batch.priority.success_rate` | % de actualizaciones exitosas | < 90% |
| `batch.priority.latency_ms` | Latencia promedio del batch | > 30000 |
| `queue.size` | Tamano de cola de secundarios | > 500 |
| `api.rate_limit_waits` | Veces que espero por rate limit | > 10/hora |

### 8.2 Endpoint de Health

```python
# api/health.py

@router.get("/health/batch")
async def batch_health(scheduler: AssetBatchScheduler):
    jobs = scheduler.get_job_status()
    queue_stats = await scheduler.update_service.queue.get_queue_stats()

    return {
        "status": "healthy",
        "jobs": jobs,
        "queue": queue_stats,
        "last_priority_update": await get_last_priority_update(),
        "api_calls_remaining": await get_api_calls_remaining()
    }
```

---

## 9. Trazabilidad

### 9.1 Documentos Relacionados

| Documento | Tipo | Relacion |
|-----------|------|----------|
| [INT-DATA-001-data-service.md](./INT-DATA-001-data-service.md) | Integracion | Base |
| [RF-DATA-001-sincronizacion-batch.md](../RF-DATA-001-sincronizacion-batch.md) | Requerimiento | Implementa |
| [ET-DATA-001-arquitectura-batch.md](../ET-DATA-001-arquitectura-batch.md) | Especificacion | Detalla |

### 9.2 Archivos de Codigo

| Archivo | Proposito |
|---------|-----------|
| `apps/data-service/src/config/priority_assets.py` | Configuracion de activos |
| `apps/data-service/src/services/priority_queue_service.py` | Cola de prioridad |
| `apps/data-service/src/services/asset_update_service.py` | Servicio de actualizacion |
| `apps/data-service/src/services/batch_scheduler.py` | Scheduler del batch |

---

## 10. Historial de Cambios

| Version | Fecha | Autor | Cambios |
|---------|-------|-------|---------|
| 1.0.0 | 2026-01-04 | Orquestador Agent | Creacion inicial |

---

**Estado de Implementacion:**

| Aspecto | Estado | Notas |
|---------|--------|-------|
| Documentacion | ✅ | Este documento |
| Diseno tecnico | ✅ | Arquitectura definida |
| Codigo Python | ⏳ | Pendiente |
| Tests | ⏳ | Pendiente |
| Integracion | ⏳ | Pendiente |
