--- id: "ET-LLM-005" title: "Arquitectura del Sistema de Tools" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-007" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-LLM-005: Arquitectura del Sistema de Tools **Épica:** OQI-007 - LLM Strategy Agent **Versión:** 1.0 **Fecha:** 2025-12-05 **Estado:** Planificado **Prioridad:** P0 - Crítico --- ## Resumen Esta especificación define la arquitectura del sistema de tools (herramientas) que el agente LLM puede invocar para obtener información y ejecutar acciones en la plataforma. --- ## Arquitectura General ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ TOOLS SYSTEM │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Tool Registry │ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Market │ │ Portfolio │ │ News │ │ ML │ │ │ │ │ │ Tools │ │ Tools │ │ Tools │ │ Tools │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Trading │ │ Alert │ │ Calculate │ │ Education │ │ │ │ │ │ Tools │ │ Tools │ │ Tools │ │ Tools │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Tool Executor │ │ │ │ │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐ │ │ │ │ │ Permission │ │ Rate │ │ Execution │ │ │ │ │ │ Checker │ │ Limiter │ │ Engine │ │ │ │ │ └──────────────┘ └──────────────┘ └──────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Service Adapters │ │ │ │ │ │ │ │ MarketData │ Portfolio │ News │ ML │ Trading │ Alerts │ ... │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Tool Registry ### Estructura de Registro ```typescript // src/modules/copilot/tools/tool-registry.ts interface ToolDefinition { id: string; name: string; description: string; category: ToolCategory; requiredPlan: 'free' | 'pro' | 'premium'; rateLimit: RateLimitConfig; parameters: ToolParameters; handler: ToolHandler; validator?: ToolValidator; postProcessor?: ToolPostProcessor; } type ToolCategory = | 'market' | 'portfolio' | 'news' | 'ml' | 'trading' | 'alerts' | 'calculate' | 'education'; interface RateLimitConfig { requestsPerMinute: number; requestsPerHour?: number; requestsPerDay?: number; } @Injectable() export class ToolRegistry { private tools: Map = new Map(); constructor( private readonly marketTools: MarketToolsProvider, private readonly portfolioTools: PortfolioToolsProvider, private readonly newsTools: NewsToolsProvider, private readonly mlTools: MLToolsProvider, private readonly tradingTools: TradingToolsProvider, private readonly alertTools: AlertToolsProvider, private readonly calculateTools: CalculateToolsProvider, private readonly educationTools: EducationToolsProvider, ) { this.registerAllTools(); } private registerAllTools(): void { // Register market tools this.marketTools.getTools().forEach(tool => this.register(tool)); // Register portfolio tools this.portfolioTools.getTools().forEach(tool => this.register(tool)); // Register all other tools... this.newsTools.getTools().forEach(tool => this.register(tool)); this.mlTools.getTools().forEach(tool => this.register(tool)); this.tradingTools.getTools().forEach(tool => this.register(tool)); this.alertTools.getTools().forEach(tool => this.register(tool)); this.calculateTools.getTools().forEach(tool => this.register(tool)); this.educationTools.getTools().forEach(tool => this.register(tool)); } register(tool: ToolDefinition): void { if (this.tools.has(tool.id)) { throw new Error(`Tool ${tool.id} already registered`); } this.tools.set(tool.id, tool); } get(toolId: string): ToolDefinition | undefined { return this.tools.get(toolId); } getByCategory(category: ToolCategory): ToolDefinition[] { return Array.from(this.tools.values()).filter(t => t.category === category); } getAvailableForPlan(plan: string): ToolDefinition[] { const planHierarchy = { free: 0, pro: 1, premium: 2 }; const userPlanLevel = planHierarchy[plan] || 0; return Array.from(this.tools.values()).filter(tool => { const toolPlanLevel = planHierarchy[tool.requiredPlan] || 0; return toolPlanLevel <= userPlanLevel; }); } getOpenAIToolDefinitions(plan: string): OpenAITool[] { return this.getAvailableForPlan(plan).map(tool => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters, }, })); } } ``` ### Catálogo Completo de Tools ```typescript // src/modules/copilot/tools/catalog.ts export const TOOL_CATALOG: ToolDefinition[] = [ // ============================================ // MARKET TOOLS // ============================================ { id: 'market.get_price', name: 'get_price', description: 'Obtiene el precio actual de un símbolo con cambio 24h y volumen', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 60 }, parameters: { type: 'object', properties: { symbol: { type: 'string', description: 'Símbolo del activo (ej: AAPL, BTC/USD, ETH/USD)', }, }, required: ['symbol'], }, handler: 'marketService.getPrice', }, { id: 'market.get_ohlcv', name: 'get_ohlcv', description: 'Obtiene datos históricos OHLCV (velas) de un símbolo', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: { symbol: { type: 'string', description: 'Símbolo del activo' }, timeframe: { type: 'string', enum: ['1m', '5m', '15m', '1h', '4h', '1d', '1w'], description: 'Intervalo temporal de las velas', }, limit: { type: 'number', description: 'Número de velas (máximo 500)', default: 100, }, }, required: ['symbol', 'timeframe'], }, handler: 'marketService.getOHLCV', }, { id: 'market.get_indicators', name: 'get_indicators', description: 'Calcula indicadores técnicos para un símbolo', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, indicators: { type: 'array', items: { type: 'string', enum: ['RSI', 'MACD', 'BB', 'SMA', 'EMA', 'ATR', 'VWAP', 'OBV', 'ADX'], }, }, timeframe: { type: 'string', enum: ['1h', '4h', '1d'], default: '1d', }, }, required: ['symbol', 'indicators'], }, handler: 'marketService.getIndicators', }, { id: 'market.get_fundamentals', name: 'get_fundamentals', description: 'Obtiene datos fundamentales de una acción (P/E, revenue, etc.)', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { symbol: { type: 'string', description: 'Símbolo de la acción' }, }, required: ['symbol'], }, handler: 'marketService.getFundamentals', }, // ============================================ // NEWS TOOLS // ============================================ { id: 'news.get_news', name: 'get_news', description: 'Obtiene noticias recientes con análisis de sentimiento', category: 'news', requiredPlan: 'free', rateLimit: { requestsPerMinute: 10 }, parameters: { type: 'object', properties: { symbol: { type: 'string', description: 'Símbolo para buscar noticias' }, limit: { type: 'number', default: 5, description: 'Número de noticias' }, }, required: ['symbol'], }, handler: 'newsService.getBySymbol', }, { id: 'news.get_sentiment', name: 'get_sentiment', description: 'Obtiene análisis de sentimiento agregado de múltiples fuentes', category: 'news', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, }, required: ['symbol'], }, handler: 'newsService.getSentiment', }, // ============================================ // ML TOOLS (Pro/Premium) // ============================================ { id: 'ml.get_prediction', name: 'get_ml_signals', description: 'Obtiene predicciones del modelo ML con nivel de confianza', category: 'ml', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, }, required: ['symbol'], }, handler: 'mlService.getPrediction', }, { id: 'ml.get_features', name: 'get_ml_features', description: 'Obtiene las features principales usadas en la predicción', category: 'ml', requiredPlan: 'premium', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, prediction_id: { type: 'string', description: 'ID de predicción previa' }, }, required: ['symbol'], }, handler: 'mlService.getFeatureImportance', }, // ============================================ // PORTFOLIO TOOLS (Pro/Premium) // ============================================ { id: 'portfolio.get_positions', name: 'get_portfolio', description: 'Obtiene las posiciones actuales del usuario', category: 'portfolio', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: {}, }, handler: 'portfolioService.getPositions', }, { id: 'portfolio.get_history', name: 'get_trade_history', description: 'Obtiene el historial de trades del usuario', category: 'portfolio', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { limit: { type: 'number', default: 20 }, symbol: { type: 'string', description: 'Filtrar por símbolo (opcional)' }, }, }, handler: 'portfolioService.getHistory', }, // ============================================ // TRADING TOOLS (Pro/Premium) // ============================================ { id: 'trading.create_paper_order', name: 'create_paper_order', description: 'Crea una orden de paper trading (requiere confirmación)', category: 'trading', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 10 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, side: { type: 'string', enum: ['buy', 'sell'] }, quantity: { type: 'number' }, order_type: { type: 'string', enum: ['market', 'limit', 'stop', 'stop_limit'], }, limit_price: { type: 'number', description: 'Precio límite (para limit orders)' }, stop_price: { type: 'number', description: 'Precio stop (para stop orders)' }, }, required: ['symbol', 'side', 'quantity', 'order_type'], }, handler: 'tradingService.createPaperOrder', validator: 'tradingValidator.validateOrder', postProcessor: 'tradingService.requireConfirmation', }, { id: 'trading.cancel_paper_order', name: 'cancel_paper_order', description: 'Cancela una orden de paper trading pendiente', category: 'trading', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { order_id: { type: 'string' }, }, required: ['order_id'], }, handler: 'tradingService.cancelPaperOrder', }, { id: 'trading.get_pending_orders', name: 'get_pending_orders', description: 'Obtiene las órdenes pendientes del usuario', category: 'trading', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: {}, }, handler: 'tradingService.getPendingOrders', }, // ============================================ // ALERT TOOLS // ============================================ { id: 'alert.create', name: 'create_alert', description: 'Crea una alerta de precio', category: 'alerts', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, condition: { type: 'string', enum: ['>=', '<=', '=='] }, price: { type: 'number' }, message: { type: 'string', description: 'Mensaje opcional' }, }, required: ['symbol', 'condition', 'price'], }, handler: 'alertService.create', }, { id: 'alert.list', name: 'list_alerts', description: 'Lista las alertas activas del usuario', category: 'alerts', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: { symbol: { type: 'string', description: 'Filtrar por símbolo (opcional)' }, }, }, handler: 'alertService.list', }, { id: 'alert.delete', name: 'delete_alert', description: 'Elimina una alerta', category: 'alerts', requiredPlan: 'pro', rateLimit: { requestsPerMinute: 20 }, parameters: { type: 'object', properties: { alert_id: { type: 'string' }, }, required: ['alert_id'], }, handler: 'alertService.delete', }, // ============================================ // CALCULATE TOOLS // ============================================ { id: 'calculate.position_size', name: 'calculate_position_size', description: 'Calcula el tamaño de posición basado en riesgo', category: 'calculate', requiredPlan: 'free', rateLimit: { requestsPerMinute: 100 }, parameters: { type: 'object', properties: { capital: { type: 'number', description: 'Capital disponible' }, entry_price: { type: 'number' }, stop_loss_price: { type: 'number' }, risk_percent: { type: 'number', description: 'Porcentaje de riesgo (ej: 0.02 para 2%)' }, }, required: ['capital', 'entry_price', 'stop_loss_price', 'risk_percent'], }, handler: 'calculateService.positionSize', }, { id: 'calculate.risk_reward', name: 'calculate_risk_reward', description: 'Calcula el ratio riesgo/beneficio de una operación', category: 'calculate', requiredPlan: 'free', rateLimit: { requestsPerMinute: 100 }, parameters: { type: 'object', properties: { entry_price: { type: 'number' }, stop_loss_price: { type: 'number' }, take_profit_price: { type: 'number' }, }, required: ['entry_price', 'stop_loss_price', 'take_profit_price'], }, handler: 'calculateService.riskReward', }, // ============================================ // WATCHLIST TOOLS // ============================================ { id: 'watchlist.get', name: 'get_watchlist', description: 'Obtiene la watchlist del usuario', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 60 }, parameters: { type: 'object', properties: {}, }, handler: 'watchlistService.get', }, { id: 'watchlist.add', name: 'add_to_watchlist', description: 'Agrega un símbolo a la watchlist', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, }, required: ['symbol'], }, handler: 'watchlistService.add', }, { id: 'watchlist.remove', name: 'remove_from_watchlist', description: 'Elimina un símbolo de la watchlist', category: 'market', requiredPlan: 'free', rateLimit: { requestsPerMinute: 30 }, parameters: { type: 'object', properties: { symbol: { type: 'string' }, }, required: ['symbol'], }, handler: 'watchlistService.remove', }, ]; ``` --- ## Tool Executor ```typescript // src/modules/copilot/tools/tool-executor.ts interface ToolExecutionContext { userId: string; userPlan: string; conversationId: string; toolCallId: string; } interface ToolExecutionResult { success: boolean; data?: any; error?: { code: string; message: string; }; metadata: { executionTime: number; cached: boolean; }; } @Injectable() export class ToolExecutor { constructor( private readonly registry: ToolRegistry, private readonly permissionChecker: PermissionChecker, private readonly rateLimiter: RateLimiter, private readonly cache: CacheService, private readonly logger: LoggerService, // Service adapters private readonly marketService: MarketService, private readonly portfolioService: PortfolioService, private readonly newsService: NewsService, private readonly mlService: MLService, private readonly tradingService: TradingService, private readonly alertService: AlertService, private readonly calculateService: CalculateService, private readonly watchlistService: WatchlistService, ) {} async execute( toolName: string, parameters: Record, context: ToolExecutionContext, ): Promise { const startTime = Date.now(); // 1. Get tool definition const tool = this.registry.get(toolName); if (!tool) { return { success: false, error: { code: 'TOOL_NOT_FOUND', message: `Tool ${toolName} not found` }, metadata: { executionTime: 0, cached: false }, }; } // 2. Check permissions const hasPermission = await this.permissionChecker.check( context.userId, context.userPlan, tool.requiredPlan, ); if (!hasPermission) { return { success: false, error: { code: 'PLAN_REQUIRED', message: `Tool ${toolName} requires ${tool.requiredPlan} plan`, }, metadata: { executionTime: Date.now() - startTime, cached: false }, }; } // 3. Check rate limit const rateLimitOk = await this.rateLimiter.check( context.userId, tool.id, tool.rateLimit, ); if (!rateLimitOk) { return { success: false, error: { code: 'RATE_LIMIT', message: `Rate limit exceeded for ${toolName}`, }, metadata: { executionTime: Date.now() - startTime, cached: false }, }; } // 4. Validate parameters if (tool.validator) { const validationResult = await this.validateParameters( tool.validator, parameters, context, ); if (!validationResult.valid) { return { success: false, error: { code: 'VALIDATION_ERROR', message: validationResult.message, }, metadata: { executionTime: Date.now() - startTime, cached: false }, }; } } // 5. Check cache (for read-only tools) if (this.isCacheable(tool)) { const cacheKey = this.buildCacheKey(tool.id, parameters, context.userId); const cached = await this.cache.get(cacheKey); if (cached) { return { success: true, data: cached, metadata: { executionTime: Date.now() - startTime, cached: true }, }; } } // 6. Execute tool try { const result = await this.executeHandler(tool, parameters, context); // 7. Post-process if needed let finalResult = result; if (tool.postProcessor) { finalResult = await this.postProcess(tool.postProcessor, result, context); } // 8. Cache result if applicable if (this.isCacheable(tool)) { const cacheKey = this.buildCacheKey(tool.id, parameters, context.userId); await this.cache.set(cacheKey, finalResult, this.getCacheTTL(tool)); } // 9. Log execution await this.logger.logToolExecution({ userId: context.userId, toolId: tool.id, parameters, success: true, executionTime: Date.now() - startTime, }); return { success: true, data: finalResult, metadata: { executionTime: Date.now() - startTime, cached: false }, }; } catch (error) { await this.logger.logToolExecution({ userId: context.userId, toolId: tool.id, parameters, success: false, error: error.message, executionTime: Date.now() - startTime, }); return { success: false, error: { code: 'EXECUTION_ERROR', message: error.message, }, metadata: { executionTime: Date.now() - startTime, cached: false }, }; } } private async executeHandler( tool: ToolDefinition, parameters: Record, context: ToolExecutionContext, ): Promise { const [serviceName, methodName] = tool.handler.split('.'); const service = this.getService(serviceName); if (!service || !service[methodName]) { throw new Error(`Handler ${tool.handler} not found`); } // Add context to parameters for handlers that need it const enrichedParams = { ...parameters, _context: { userId: context.userId, userPlan: context.userPlan, }, }; return service[methodName](enrichedParams); } private getService(serviceName: string): any { const services: Record = { marketService: this.marketService, portfolioService: this.portfolioService, newsService: this.newsService, mlService: this.mlService, tradingService: this.tradingService, alertService: this.alertService, calculateService: this.calculateService, watchlistService: this.watchlistService, }; return services[serviceName]; } private isCacheable(tool: ToolDefinition): boolean { const nonCacheableCategories = ['trading', 'alerts']; return !nonCacheableCategories.includes(tool.category); } private getCacheTTL(tool: ToolDefinition): number { const ttls: Record = { market: 5, // 5 seconds news: 300, // 5 minutes ml: 60, // 1 minute portfolio: 10, // 10 seconds calculate: 0, // No cache (pure function) }; return ttls[tool.category] || 30; } private buildCacheKey( toolId: string, params: Record, userId: string, ): string { const paramsHash = JSON.stringify(params); return `tool:${toolId}:${userId}:${paramsHash}`; } } ``` --- ## Rate Limiter ```typescript // src/modules/copilot/tools/rate-limiter.ts @Injectable() export class RateLimiter { constructor(private readonly redis: RedisService) {} async check( userId: string, toolId: string, config: RateLimitConfig, ): Promise { const now = Date.now(); const minute = Math.floor(now / 60000); const hour = Math.floor(now / 3600000); const day = Math.floor(now / 86400000); // Check per-minute limit if (config.requestsPerMinute) { const key = `ratelimit:${toolId}:${userId}:min:${minute}`; const count = await this.redis.incr(key); if (count === 1) { await this.redis.expire(key, 60); } if (count > config.requestsPerMinute) { return false; } } // Check per-hour limit if (config.requestsPerHour) { const key = `ratelimit:${toolId}:${userId}:hour:${hour}`; const count = await this.redis.incr(key); if (count === 1) { await this.redis.expire(key, 3600); } if (count > config.requestsPerHour) { return false; } } // Check per-day limit if (config.requestsPerDay) { const key = `ratelimit:${toolId}:${userId}:day:${day}`; const count = await this.redis.incr(key); if (count === 1) { await this.redis.expire(key, 86400); } if (count > config.requestsPerDay) { return false; } } return true; } async getRemainingQuota( userId: string, toolId: string, config: RateLimitConfig, ): Promise { const now = Date.now(); const minute = Math.floor(now / 60000); const key = `ratelimit:${toolId}:${userId}:min:${minute}`; const used = parseInt(await this.redis.get(key) || '0', 10); return { remaining: Math.max(0, config.requestsPerMinute - used), resetAt: (minute + 1) * 60000, }; } } ``` --- ## Permission Checker ```typescript // src/modules/copilot/tools/permission-checker.ts @Injectable() export class PermissionChecker { private readonly planHierarchy = { free: 0, pro: 1, premium: 2, }; async check( userId: string, userPlan: string, requiredPlan: string, ): Promise { const userLevel = this.planHierarchy[userPlan] ?? 0; const requiredLevel = this.planHierarchy[requiredPlan] ?? 0; return userLevel >= requiredLevel; } getUpgradeMessage(currentPlan: string, requiredPlan: string): string { if (currentPlan === 'free' && requiredPlan === 'pro') { return 'Esta función requiere el plan Pro. Actualiza para acceder a señales ML, paper trading y más.'; } if (requiredPlan === 'premium') { return 'Esta función requiere el plan Premium. Actualiza para acceso completo a todas las funcionalidades.'; } return `Esta función requiere el plan ${requiredPlan}.`; } } ``` --- ## Diagrama de Flujo de Ejecución ``` ┌─────────────────────────────────────────────────────────────┐ │ LLM genera tool_call: get_ml_signals({ symbol: "AAPL" }) │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 1. Tool Registry: Buscar definición de "get_ml_signals" │ │ → Encontrado: ml.get_prediction │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 2. Permission Checker: ¿Usuario tiene plan Pro+? │ │ → User plan: "pro" >= required: "pro" ✓ │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 3. Rate Limiter: ¿Dentro del límite (30/min)? │ │ → Requests this minute: 5 < 30 ✓ │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 4. Cache Check: ¿Resultado cacheado? │ │ → Cache miss (TTL expirado) │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 5. Execute Handler: mlService.getPrediction({ symbol }) │ │ → Llamada al ML Engine │ │ → Response: { prediction: "bullish", confidence: 0.72 } │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 6. Cache Store: Guardar resultado (TTL: 60s) │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 7. Log Execution: Registrar uso de tool │ └──────────────────────────┬──────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ 8. Return to LLM: { success: true, data: {...} } │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Dependencias ### Servicios Internos - MarketService (OQI-003) - PortfolioService (OQI-004) - MLService (OQI-006) - AlertService (OQI-003) - TradingService (OQI-003) ### Infraestructura - Redis (rate limiting, cache) - PostgreSQL (logs de ejecución) --- ## Referencias - [RF-LLM-005: Tool Integration](../requerimientos/RF-LLM-005-tool-integration.md) - [ET-LLM-002: Agente de Análisis](./ET-LLM-002-agente-analisis.md) --- *Especificación técnica - Sistema NEXUS* *Trading Platform*