# ET-TRD-001: Especificación Técnica - Market Data Integration

**Version:** 1.0.0
**Fecha:** 2025-12-05
**Estado:** Pendiente
**Épica:** [OQI-003](../_MAP.md)
**Requerimiento:** RF-TRD-001

---

## Resumen

Esta especificación detalla la implementación técnica del sistema de obtención y gestión de datos de mercado en tiempo real desde Binance API, incluyendo caching, rate limiting y optimización de consultas.

---

## Arquitectura

```
┌─────────────────────────────────────────────────────────────────────────┐
│                              FRONTEND                                    │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐     │
│  │ TradingPage.tsx │───▶│ ChartComponent  │───▶│ tradingStore    │     │
│  │                 │    │ .tsx            │    │ (Zustand)       │     │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘     │
└────────────────────────────────┬────────────────────────────────────────┘
                                 │
                                 │ HTTPS/WSS
                                 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                              BACKEND                                     │
│  ┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐     │
│  │ market.routes   │───▶│ binance.service │───▶│ cache.service   │     │
│  │ .ts             │    │ .ts             │    │ .ts (Redis)     │     │
│  └─────────────────┘    └─────────────────┘    └─────────────────┘     │
│           │                     │                      │                │
│           │                     │                      │                │
│           ▼                     ▼                      ▼                │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                     PostgreSQL (trading schema)                  │   │
│  │  ┌──────────────┐  ┌────────────────┐  ┌──────────────────┐    │   │
│  │  │ market_data  │  │  rate_limits   │  │  api_requests    │    │   │
│  │  └──────────────┘  └────────────────┘  └──────────────────┘    │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
                                 │
                                 ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                         BINANCE API                                      │
│  ┌─────────────────────┐  ┌────────────────────────────────────────┐   │
│  │ REST API            │  │ WebSocket API                          │   │
│  │ /api/v3/klines      │  │ wss://stream.binance.com:9443/ws       │   │
│  │ /api/v3/ticker/24hr │  │ <symbol>@kline_<interval>              │   │
│  │ /api/v3/depth       │  │ <symbol>@ticker                        │   │
│  └─────────────────────┘  └────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## Componentes

### 1. Binance Service (`binance.service.ts`)

**Ubicación:** `apps/backend/src/modules/trading/services/binance.service.ts`

```typescript
import axios, { AxiosInstance } from 'axios';
import { createHmac } from 'crypto';

export interface Kline {
  openTime: number;
  open: string;
  high: string;
  low: string;
  close: string;
  volume: string;
  closeTime: number;
  quoteVolume: string;
  trades: number;
  takerBuyBaseVolume: string;
  takerBuyQuoteVolume: string;
}

export interface Ticker24hr {
  symbol: string;
  priceChange: string;
  priceChangePercent: string;
  weightedAvgPrice: string;
  lastPrice: string;
  lastQty: string;
  bidPrice: string;
  bidQty: string;
  askPrice: string;
  askQty: string;
  openPrice: string;
  highPrice: string;
  lowPrice: string;
  volume: string;
  quoteVolume: string;
  openTime: number;
  closeTime: number;
  firstId: number;
  lastId: number;
  count: number;
}

export interface OrderBook {
  lastUpdateId: number;
  bids: [string, string][];  // [price, quantity]
  asks: [string, string][];
}

export class BinanceService {
  private client: AxiosInstance;
  private apiKey: string;
  private apiSecret: string;
  private baseURL = 'https://api.binance.com';

  constructor() {
    this.apiKey = process.env.BINANCE_API_KEY || '';
    this.apiSecret = process.env.BINANCE_API_SECRET || '';

    this.client = axios.create({
      baseURL: this.baseURL,
      timeout: 10000,
      headers: {
        'X-MBX-APIKEY': this.apiKey,
      },
    });

    // Request interceptor para rate limiting
    this.client.interceptors.request.use(
      async (config) => {
        await this.checkRateLimit();
        return config;
      }
    );
  }

  // Obtener klines/candles históricos
  async getKlines(params: {
    symbol: string;
    interval: KlineInterval;
    startTime?: number;
    endTime?: number;
    limit?: number;  // Max 1000
  }): Promise<Kline[]> {
    const response = await this.client.get('/api/v3/klines', { params });
    return response.data.map(this.parseKline);
  }

  // Obtener ticker 24hr
  async getTicker24hr(symbol: string): Promise<Ticker24hr> {
    const response = await this.client.get('/api/v3/ticker/24hr', {
      params: { symbol },
    });
    return response.data;
  }

  // Obtener múltiples tickers
  async getAllTickers(): Promise<Ticker24hr[]> {
    const response = await this.client.get('/api/v3/ticker/24hr');
    return response.data;
  }

  // Obtener order book
  async getOrderBook(symbol: string, limit: number = 100): Promise<OrderBook> {
    const response = await this.client.get('/api/v3/depth', {
      params: { symbol, limit },
    });
    return response.data;
  }

  // Obtener precio actual
  async getCurrentPrice(symbol: string): Promise<string> {
    const response = await this.client.get('/api/v3/ticker/price', {
      params: { symbol },
    });
    return response.data.price;
  }

  // Obtener información de símbolos disponibles
  async getExchangeInfo(symbol?: string): Promise<any> {
    const params = symbol ? { symbol } : {};
    const response = await this.client.get('/api/v3/exchangeInfo', { params });
    return response.data;
  }

  // Verificar conectividad
  async ping(): Promise<boolean> {
    try {
      await this.client.get('/api/v3/ping');
      return true;
    } catch {
      return false;
    }
  }

  // Obtener tiempo del servidor
  async getServerTime(): Promise<number> {
    const response = await this.client.get('/api/v3/time');
    return response.data.serverTime;
  }

  private parseKline(data: any[]): Kline {
    return {
      openTime: data[0],
      open: data[1],
      high: data[2],
      low: data[3],
      close: data[4],
      volume: data[5],
      closeTime: data[6],
      quoteVolume: data[7],
      trades: data[8],
      takerBuyBaseVolume: data[9],
      takerBuyQuoteVolume: data[10],
    };
  }

  private async checkRateLimit(): Promise<void> {
    // Implementar rate limiting según límites de Binance
    // 1200 requests per minute con weight
  }
}

export type KlineInterval =
  | '1s' | '1m' | '3m' | '5m' | '15m' | '30m'
  | '1h' | '2h' | '4h' | '6h' | '8h' | '12h'
  | '1d' | '3d' | '1w' | '1M';
```

### 2. Cache Service (`cache.service.ts`)

**Ubicación:** `apps/backend/src/modules/trading/services/cache.service.ts`

```typescript
import Redis from 'ioredis';

export class MarketDataCacheService {
  private redis: Redis;

  constructor() {
    this.redis = new Redis({
      host: process.env.REDIS_HOST || 'localhost',
      port: parseInt(process.env.REDIS_PORT || '6379'),
      password: process.env.REDIS_PASSWORD,
      db: 1, // Usar DB 1 para market data
    });
  }

  // Cache para klines
  async cacheKlines(
    symbol: string,
    interval: string,
    klines: Kline[],
    ttl: number = 60
  ): Promise<void> {
    const key = `klines:${symbol}:${interval}`;
    await this.redis.setex(key, ttl, JSON.stringify(klines));
  }

  async getCachedKlines(
    symbol: string,
    interval: string
  ): Promise<Kline[] | null> {
    const key = `klines:${symbol}:${interval}`;
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  // Cache para ticker
  async cacheTicker(symbol: string, ticker: Ticker24hr, ttl: number = 5): Promise<void> {
    const key = `ticker:${symbol}`;
    await this.redis.setex(key, ttl, JSON.stringify(ticker));
  }

  async getCachedTicker(symbol: string): Promise<Ticker24hr | null> {
    const key = `ticker:${symbol}`;
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  // Cache para order book
  async cacheOrderBook(
    symbol: string,
    orderBook: OrderBook,
    ttl: number = 2
  ): Promise<void> {
    const key = `orderbook:${symbol}`;
    await this.redis.setex(key, ttl, JSON.stringify(orderBook));
  }

  async getCachedOrderBook(symbol: string): Promise<OrderBook | null> {
    const key = `orderbook:${symbol}`;
    const data = await this.redis.get(key);
    return data ? JSON.parse(data) : null;
  }

  // Cache para lista de símbolos
  async cacheSymbols(symbols: any[], ttl: number = 3600): Promise<void> {
    await this.redis.setex('symbols:all', ttl, JSON.stringify(symbols));
  }

  async getCachedSymbols(): Promise<any[] | null> {
    const data = await this.redis.get('symbols:all');
    return data ? JSON.parse(data) : null;
  }

  // Invalidar cache
  async invalidateKlines(symbol: string, interval?: string): Promise<void> {
    if (interval) {
      await this.redis.del(`klines:${symbol}:${interval}`);
    } else {
      const keys = await this.redis.keys(`klines:${symbol}:*`);
      if (keys.length > 0) {
        await this.redis.del(...keys);
      }
    }
  }

  async invalidateTicker(symbol: string): Promise<void> {
    await this.redis.del(`ticker:${symbol}`);
  }
}
```

### 3. Market Data Routes

**Ubicación:** `apps/backend/src/modules/trading/market.routes.ts`

```typescript
import { Router } from 'express';
import { MarketDataController } from './controllers/market-data.controller';
import { authenticate } from '@/middleware/auth';
import { validateRequest } from '@/middleware/validation';
import { getKlinesSchema, getTickerSchema } from './validation/market.schemas';

const router = Router();
const controller = new MarketDataController();

// Obtener klines/candles
router.get(
  '/klines',
  authenticate,
  validateRequest(getKlinesSchema),
  controller.getKlines
);

// Obtener ticker 24hr
router.get(
  '/ticker/:symbol',
  authenticate,
  controller.getTicker
);

// Obtener todos los tickers
router.get(
  '/tickers',
  authenticate,
  controller.getAllTickers
);

// Obtener order book
router.get(
  '/orderbook/:symbol',
  authenticate,
  controller.getOrderBook
);

// Obtener precio actual
router.get(
  '/price/:symbol',
  authenticate,
  controller.getCurrentPrice
);

// Obtener símbolos disponibles
router.get(
  '/symbols',
  authenticate,
  controller.getSymbols
);

// Health check
router.get(
  '/health',
  controller.healthCheck
);

export default router;
```

---

## Configuración

### Variables de Entorno

```bash
# .env
# Binance API
BINANCE_API_KEY=your_api_key_here
BINANCE_API_SECRET=your_api_secret_here
BINANCE_BASE_URL=https://api.binance.com
BINANCE_WS_URL=wss://stream.binance.com:9443/ws

# Redis Cache
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB_MARKET_DATA=1

# Rate Limiting
BINANCE_MAX_REQUESTS_PER_MINUTE=1200
BINANCE_MAX_WEIGHT_PER_MINUTE=6000
```

### Rate Limiting Configuration

```typescript
// config/rate-limit.config.ts
export const binanceRateLimits = {
  // Request limits
  requests: {
    perMinute: 1200,
    perSecond: 20,
  },

  // Weight limits
  weight: {
    perMinute: 6000,
  },

  // Endpoint weights
  weights: {
    '/api/v3/klines': 1,
    '/api/v3/ticker/24hr': 1,  // single symbol
    '/api/v3/ticker/24hr_all': 40,  // all symbols
    '/api/v3/depth': (limit: number) => {
      if (limit <= 100) return 1;
      if (limit <= 500) return 5;
      if (limit <= 1000) return 10;
      return 50;
    },
  },
};
```

---

## Esquema de Base de Datos

```sql
-- Schema trading
CREATE SCHEMA IF NOT EXISTS trading;

-- Tabla para cachear datos históricos (opcional)
CREATE TABLE trading.market_data (
    id BIGSERIAL PRIMARY KEY,
    symbol VARCHAR(20) NOT NULL,
    interval VARCHAR(10) NOT NULL,
    open_time BIGINT NOT NULL,
    open DECIMAL(20, 8) NOT NULL,
    high DECIMAL(20, 8) NOT NULL,
    low DECIMAL(20, 8) NOT NULL,
    close DECIMAL(20, 8) NOT NULL,
    volume DECIMAL(20, 8) NOT NULL,
    close_time BIGINT NOT NULL,
    quote_volume DECIMAL(20, 8),
    trades INTEGER,
    taker_buy_base_volume DECIMAL(20, 8),
    taker_buy_quote_volume DECIMAL(20, 8),
    created_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(symbol, interval, open_time)
);

-- Índices
CREATE INDEX idx_market_data_symbol ON trading.market_data(symbol);
CREATE INDEX idx_market_data_interval ON trading.market_data(interval);
CREATE INDEX idx_market_data_time ON trading.market_data(open_time DESC);
CREATE INDEX idx_market_data_composite ON trading.market_data(symbol, interval, open_time DESC);

-- Tabla para tracking de rate limits
CREATE TABLE trading.rate_limits (
    id SERIAL PRIMARY KEY,
    endpoint VARCHAR(100) NOT NULL,
    window_start TIMESTAMPTZ NOT NULL,
    request_count INTEGER DEFAULT 0,
    weight_used INTEGER DEFAULT 0,
    created_at TIMESTAMPTZ DEFAULT NOW(),

    UNIQUE(endpoint, window_start)
);

-- Tabla para log de requests (debugging)
CREATE TABLE trading.api_requests (
    id BIGSERIAL PRIMARY KEY,
    user_id UUID REFERENCES public.users(id),
    endpoint VARCHAR(200) NOT NULL,
    method VARCHAR(10) NOT NULL,
    params JSONB,
    response_time INTEGER,  -- ms
    status_code INTEGER,
    error TEXT,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_api_requests_user ON trading.api_requests(user_id);
CREATE INDEX idx_api_requests_created ON trading.api_requests(created_at DESC);
```

---

## Implementación del Controller

```typescript
// controllers/market-data.controller.ts
import { Request, Response } from 'express';
import { BinanceService } from '../services/binance.service';
import { MarketDataCacheService } from '../services/cache.service';

export class MarketDataController {
  private binanceService: BinanceService;
  private cacheService: MarketDataCacheService;

  constructor() {
    this.binanceService = new BinanceService();
    this.cacheService = new MarketDataCacheService();
  }

  getKlines = async (req: Request, res: Response) => {
    const { symbol, interval, startTime, endTime, limit } = req.query;

    try {
      // Verificar cache
      const cached = await this.cacheService.getCachedKlines(
        symbol as string,
        interval as string
      );

      if (cached) {
        return res.json({
          data: cached,
          cached: true,
        });
      }

      // Obtener de Binance
      const klines = await this.binanceService.getKlines({
        symbol: symbol as string,
        interval: interval as KlineInterval,
        startTime: startTime ? parseInt(startTime as string) : undefined,
        endTime: endTime ? parseInt(endTime as string) : undefined,
        limit: limit ? parseInt(limit as string) : 500,
      });

      // Cachear resultado
      await this.cacheService.cacheKlines(
        symbol as string,
        interval as string,
        klines,
        this.getCacheTTL(interval as string)
      );

      res.json({
        data: klines,
        cached: false,
      });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  };

  getTicker = async (req: Request, res: Response) => {
    const { symbol } = req.params;

    try {
      // Verificar cache
      const cached = await this.cacheService.getCachedTicker(symbol);
      if (cached) {
        return res.json({ data: cached, cached: true });
      }

      // Obtener de Binance
      const ticker = await this.binanceService.getTicker24hr(symbol);

      // Cachear (TTL corto para datos en tiempo real)
      await this.cacheService.cacheTicker(symbol, ticker, 5);

      res.json({ data: ticker, cached: false });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  };

  getAllTickers = async (req: Request, res: Response) => {
    try {
      const tickers = await this.binanceService.getAllTickers();
      res.json({ data: tickers });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  };

  getOrderBook = async (req: Request, res: Response) => {
    const { symbol } = req.params;
    const { limit = 100 } = req.query;

    try {
      const cached = await this.cacheService.getCachedOrderBook(symbol);
      if (cached) {
        return res.json({ data: cached, cached: true });
      }

      const orderBook = await this.binanceService.getOrderBook(
        symbol,
        parseInt(limit as string)
      );

      await this.cacheService.cacheOrderBook(symbol, orderBook, 2);

      res.json({ data: orderBook, cached: false });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  };

  getCurrentPrice = async (req: Request, res: Response) => {
    const { symbol } = req.params;

    try {
      const price = await this.binanceService.getCurrentPrice(symbol);
      res.json({ symbol, price });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  };

  getSymbols = async (req: Request, res: Response) => {
    try {
      const cached = await this.cacheService.getCachedSymbols();
      if (cached) {
        return res.json({ data: cached, cached: true });
      }

      const info = await this.binanceService.getExchangeInfo();
      const symbols = info.symbols.filter(s => s.status === 'TRADING');

      await this.cacheService.cacheSymbols(symbols, 3600);

      res.json({ data: symbols, cached: false });
    } catch (error) {
      res.status(500).json({ error: error.message });
    }
  };

  healthCheck = async (req: Request, res: Response) => {
    try {
      const isHealthy = await this.binanceService.ping();
      res.json({ status: isHealthy ? 'healthy' : 'unhealthy' });
    } catch (error) {
      res.status(503).json({ status: 'unhealthy', error: error.message });
    }
  };

  private getCacheTTL(interval: string): number {
    const ttls = {
      '1s': 1,
      '1m': 5,
      '5m': 30,
      '15m': 60,
      '1h': 300,
      '1d': 3600,
    };
    return ttls[interval] || 60;
  }
}
```

---

## Seguridad

### API Key Management

```typescript
// services/api-key.service.ts
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

export class ApiKeyService {
  private algorithm = 'aes-256-gcm';
  private key: Buffer;

  constructor() {
    this.key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex');
  }

  encryptApiKey(apiKey: string): string {
    const iv = randomBytes(16);
    const cipher = createCipheriv(this.algorithm, this.key, iv);

    let encrypted = cipher.update(apiKey, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    const authTag = cipher.getAuthTag();

    return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
  }

  decryptApiKey(encrypted: string): string {
    const [ivHex, authTagHex, encryptedData] = encrypted.split(':');

    const iv = Buffer.from(ivHex, 'hex');
    const authTag = Buffer.from(authTagHex, 'hex');

    const decipher = createDecipheriv(this.algorithm, this.key, iv);
    decipher.setAuthTag(authTag);

    let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}
```

### Rate Limiting Middleware

```typescript
// middleware/rate-limit.middleware.ts
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis();

export async function rateLimitMiddleware(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const userId = req.user?.id;
  const key = `rate_limit:${userId}:${Date.now() / 60000 | 0}`;

  const current = await redis.incr(key);

  if (current === 1) {
    await redis.expire(key, 60);
  }

  const limit = 100; // requests per minute per user

  if (current > limit) {
    return res.status(429).json({
      error: 'Rate limit exceeded',
      retryAfter: 60,
    });
  }

  res.setHeader('X-RateLimit-Limit', limit.toString());
  res.setHeader('X-RateLimit-Remaining', (limit - current).toString());

  next();
}
```

---

## Testing

### Unit Tests

```typescript
describe('BinanceService', () => {
  let service: BinanceService;

  beforeEach(() => {
    service = new BinanceService();
  });

  describe('getKlines', () => {
    it('should fetch klines data', async () => {
      const params = {
        symbol: 'BTCUSDT',
        interval: '1h' as KlineInterval,
        limit: 100,
      };

      const klines = await service.getKlines(params);

      expect(klines).toBeInstanceOf(Array);
      expect(klines.length).toBeLessThanOrEqual(100);
      expect(klines[0]).toHaveProperty('open');
      expect(klines[0]).toHaveProperty('close');
    });
  });

  describe('getTicker24hr', () => {
    it('should fetch 24hr ticker data', async () => {
      const ticker = await service.getTicker24hr('BTCUSDT');

      expect(ticker).toHaveProperty('symbol', 'BTCUSDT');
      expect(ticker).toHaveProperty('lastPrice');
      expect(ticker).toHaveProperty('priceChangePercent');
    });
  });
});

describe('MarketDataCacheService', () => {
  let cacheService: MarketDataCacheService;

  beforeEach(() => {
    cacheService = new MarketDataCacheService();
  });

  afterEach(async () => {
    await cacheService.invalidateKlines('BTCUSDT');
  });

  it('should cache and retrieve klines', async () => {
    const mockKlines = [
      {
        openTime: 1234567890,
        open: '50000',
        high: '51000',
        low: '49000',
        close: '50500',
        volume: '100',
        closeTime: 1234567899,
      },
    ];

    await cacheService.cacheKlines('BTCUSDT', '1h', mockKlines, 60);
    const cached = await cacheService.getCachedKlines('BTCUSDT', '1h');

    expect(cached).toEqual(mockKlines);
  });
});
```

---

## Dependencias

```json
{
  "dependencies": {
    "axios": "^1.6.0",
    "ioredis": "^5.3.2",
    "ws": "^8.16.0"
  },
  "devDependencies": {
    "@types/ws": "^8.5.10"
  }
}
```

---

## Referencias

- [Binance API Documentation](https://binance-docs.github.io/apidocs/spot/en/)
- [Binance Rate Limits](https://binance-docs.github.io/apidocs/spot/en/#limits)
- [Redis Caching Best Practices](https://redis.io/docs/manual/patterns/)
