Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
25 KiB
25 KiB
| id | title | type | status | rf_parent | epic | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|
| ET-TRD-001 | Market Data Integration | Specification | Done | RF-TRD-001 | OQI-003 | 1.0 | 2025-12-05 | 2026-01-04 |
ET-TRD-001: Especificación Técnica - Market Data Integration
Version: 1.0.0 Fecha: 2025-12-05 Estado: Pendiente Épica: OQI-003 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
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
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
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
# .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
// 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
-- 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
// 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
// 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
// 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
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
{
"dependencies": {
"axios": "^1.6.0",
"ioredis": "^5.3.2",
"ws": "^8.16.0"
},
"devDependencies": {
"@types/ws": "^8.5.10"
}
}