--- id: "ET-LLM-003" title: "Motor de Generación de Estrategias" 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-003: Motor de Generación de Estrategias **É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 motor de generación de estrategias de trading personalizadas, incluyendo análisis de perfil de usuario, cálculo de position sizing y gestión de riesgo. --- ## Arquitectura del Motor ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ STRATEGY GENERATION ENGINE │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ User Profile │ │ Market Context │ │ ML Signals │ │ │ │ Analyzer │ │ Builder │ │ Integrator │ │ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │ │ │ │ │ │ └───────────────────────┼───────────────────────┘ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Strategy Generator │ │ │ │ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ │ │ Template │ │ Position │ │ Risk │ │ Backtest │ │ │ │ │ │ Selector │ │ Sizer │ │ Calculator │ │ Validator │ │ │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ↓ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ Strategy Output Formatter │ │ │ └───────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Componentes del Sistema ### 1. User Profile Analyzer ```typescript // src/modules/copilot/strategy/profile-analyzer.ts interface UserProfile { riskLevel: 'conservative' | 'moderate' | 'aggressive'; experience: 'beginner' | 'intermediate' | 'advanced'; capital: number; maxPositions: number; restrictions: string[]; tradingStyle: 'day' | 'swing' | 'position'; accountAge: number; // días } interface ProfileConstraints { maxRiskPerTrade: number; // % del capital maxPositionSize: number; // % del capital allowedStrategies: string[]; forbiddenAssets: string[]; minExperience: number; // meses requeridos } export class ProfileAnalyzer { getConstraints(profile: UserProfile): ProfileConstraints { const constraints: ProfileConstraints = { maxRiskPerTrade: this.calculateMaxRisk(profile), maxPositionSize: this.calculateMaxPosition(profile), allowedStrategies: this.getAllowedStrategies(profile), forbiddenAssets: profile.restrictions, minExperience: 0, }; // Restricciones por nivel de riesgo switch (profile.riskLevel) { case 'conservative': constraints.maxRiskPerTrade = Math.min(constraints.maxRiskPerTrade, 0.02); constraints.maxPositionSize = Math.min(constraints.maxPositionSize, 0.10); break; case 'moderate': constraints.maxRiskPerTrade = Math.min(constraints.maxRiskPerTrade, 0.05); constraints.maxPositionSize = Math.min(constraints.maxPositionSize, 0.20); break; case 'aggressive': constraints.maxRiskPerTrade = Math.min(constraints.maxRiskPerTrade, 0.10); constraints.maxPositionSize = Math.min(constraints.maxPositionSize, 0.30); break; } // Restricciones por experiencia if (profile.experience === 'beginner') { constraints.allowedStrategies = constraints.allowedStrategies.filter( s => !['scalping', 'options', 'leveraged'].includes(s) ); } // Restricciones por antigüedad de cuenta if (profile.accountAge < 180) { // 6 meses constraints.allowedStrategies = constraints.allowedStrategies.filter( s => s !== 'leveraged' ); } return constraints; } private calculateMaxRisk(profile: UserProfile): number { const baseRisk = { conservative: 0.01, moderate: 0.03, aggressive: 0.05, }; return baseRisk[profile.riskLevel]; } private calculateMaxPosition(profile: UserProfile): number { const basePosition = { conservative: 0.05, moderate: 0.15, aggressive: 0.25, }; return basePosition[profile.riskLevel]; } private getAllowedStrategies(profile: UserProfile): string[] { const strategies = ['trend_following', 'mean_reversion', 'breakout']; if (profile.experience !== 'beginner') { strategies.push('momentum', 'swing'); } if (profile.experience === 'advanced') { strategies.push('scalping', 'options'); } return strategies; } } ``` ### 2. Strategy Templates ```typescript // src/modules/copilot/strategy/templates.ts interface StrategyTemplate { id: string; name: string; description: string; difficulty: 'easy' | 'intermediate' | 'advanced'; style: 'day' | 'swing' | 'position'; minExperience: 'beginner' | 'intermediate' | 'advanced'; requiredIndicators: string[]; entryConditions: EntryCondition[]; exitConditions: ExitCondition[]; riskManagement: RiskManagement; } export const STRATEGY_TEMPLATES: StrategyTemplate[] = [ { id: 'sma_crossover', name: 'SMA Crossover', description: 'Cruce de medias móviles simples (20/50)', difficulty: 'easy', style: 'swing', minExperience: 'beginner', requiredIndicators: ['SMA'], entryConditions: [ { type: 'crossover', indicator1: { name: 'SMA', period: 20 }, indicator2: { name: 'SMA', period: 50 }, direction: 'above', // SMA20 cruza por encima de SMA50 action: 'long', }, { type: 'crossover', indicator1: { name: 'SMA', period: 20 }, indicator2: { name: 'SMA', period: 50 }, direction: 'below', action: 'short', }, ], exitConditions: [ { type: 'stop_loss', method: 'atr_multiple', value: 2, }, { type: 'take_profit', method: 'risk_reward', value: 2, // 2:1 R:R }, ], riskManagement: { maxRiskPerTrade: 0.02, positionSizing: 'fixed_risk', }, }, { id: 'rsi_oversold', name: 'RSI Oversold Bounce', description: 'Comprar cuando RSI está sobrevendido en tendencia alcista', difficulty: 'easy', style: 'swing', minExperience: 'beginner', requiredIndicators: ['RSI', 'SMA'], entryConditions: [ { type: 'threshold', indicator: { name: 'RSI', period: 14 }, condition: 'below', value: 30, action: 'watch', }, { type: 'threshold', indicator: { name: 'RSI', period: 14 }, condition: 'crosses_above', value: 30, action: 'long', filter: { indicator: { name: 'SMA', period: 200 }, priceAbove: true, // Precio sobre SMA200 (tendencia alcista) }, }, ], exitConditions: [ { type: 'threshold', indicator: { name: 'RSI', period: 14 }, condition: 'above', value: 70, }, { type: 'stop_loss', method: 'percent', value: 0.03, // 3% }, ], riskManagement: { maxRiskPerTrade: 0.02, positionSizing: 'fixed_risk', }, }, { id: 'bollinger_squeeze', name: 'Bollinger Band Squeeze', description: 'Breakout después de contracción de volatilidad', difficulty: 'intermediate', style: 'swing', minExperience: 'intermediate', requiredIndicators: ['BB', 'ATR'], entryConditions: [ { type: 'squeeze', indicator: { name: 'BB', period: 20, stddev: 2 }, condition: 'width_below', value: 0.04, // BB width < 4% action: 'prepare', }, { type: 'breakout', indicator: { name: 'BB', period: 20, stddev: 2 }, condition: 'close_above_upper', volumeConfirm: true, action: 'long', }, ], exitConditions: [ { type: 'trailing_stop', method: 'atr_multiple', value: 2, }, { type: 'time_exit', maxBars: 10, // Máximo 10 velas }, ], riskManagement: { maxRiskPerTrade: 0.03, positionSizing: 'volatility_adjusted', }, }, { id: 'macd_divergence', name: 'MACD Divergence', description: 'Detectar divergencias entre precio y MACD', difficulty: 'advanced', style: 'swing', minExperience: 'intermediate', requiredIndicators: ['MACD', 'RSI'], entryConditions: [ { type: 'divergence', indicator: { name: 'MACD' }, priceCondition: 'lower_low', indicatorCondition: 'higher_low', action: 'long', confirmation: { indicator: { name: 'MACD' }, condition: 'histogram_positive', }, }, ], exitConditions: [ { type: 'signal', indicator: { name: 'MACD' }, condition: 'signal_crossover_down', }, { type: 'stop_loss', method: 'swing_low', buffer: 0.005, }, ], riskManagement: { maxRiskPerTrade: 0.02, positionSizing: 'fixed_risk', }, }, { id: 'ml_momentum', name: 'ML-Enhanced Momentum', description: 'Momentum con confirmación de señales ML', difficulty: 'intermediate', style: 'swing', minExperience: 'intermediate', requiredIndicators: ['RSI', 'MACD', 'ML_SIGNAL'], entryConditions: [ { type: 'ml_signal', direction: 'bullish', minConfidence: 0.65, action: 'watch', }, { type: 'momentum', indicator: { name: 'RSI', period: 14 }, condition: 'between', range: [40, 60], action: 'prepare', }, { type: 'confirmation', indicator: { name: 'MACD' }, condition: 'histogram_rising', action: 'long', }, ], exitConditions: [ { type: 'ml_signal', direction: 'bearish', minConfidence: 0.60, }, { type: 'trailing_stop', method: 'percent', value: 0.05, }, ], riskManagement: { maxRiskPerTrade: 0.025, positionSizing: 'kelly_fraction', kellyFraction: 0.25, }, }, ]; ``` ### 3. Position Sizer ```typescript // src/modules/copilot/strategy/position-sizer.ts interface PositionSizingParams { capital: number; entryPrice: number; stopLossPrice: number; maxRiskPercent: number; method: 'fixed_risk' | 'volatility_adjusted' | 'kelly_fraction'; volatility?: number; // ATR winRate?: number; // Para Kelly avgWinLoss?: number; // Para Kelly } interface PositionSize { shares: number; totalValue: number; riskAmount: number; percentOfCapital: number; } export class PositionSizer { calculate(params: PositionSizingParams): PositionSize { switch (params.method) { case 'fixed_risk': return this.fixedRisk(params); case 'volatility_adjusted': return this.volatilityAdjusted(params); case 'kelly_fraction': return this.kellyFraction(params); default: return this.fixedRisk(params); } } private fixedRisk(params: PositionSizingParams): PositionSize { const { capital, entryPrice, stopLossPrice, maxRiskPercent } = params; const riskPerShare = Math.abs(entryPrice - stopLossPrice); const maxRiskAmount = capital * maxRiskPercent; const shares = Math.floor(maxRiskAmount / riskPerShare); const totalValue = shares * entryPrice; return { shares, totalValue, riskAmount: shares * riskPerShare, percentOfCapital: (totalValue / capital) * 100, }; } private volatilityAdjusted(params: PositionSizingParams): PositionSize { const { capital, entryPrice, maxRiskPercent, volatility } = params; if (!volatility) { return this.fixedRisk(params); } // Ajustar posición inversamente a la volatilidad const baseAllocation = capital * 0.10; // 10% base const volatilityFactor = 0.02 / volatility; // Normalizar a 2% ATR const adjustedAllocation = baseAllocation * Math.min(volatilityFactor, 2); const shares = Math.floor(adjustedAllocation / entryPrice); const stopLossDistance = volatility * 2; return { shares, totalValue: shares * entryPrice, riskAmount: shares * stopLossDistance, percentOfCapital: (shares * entryPrice / capital) * 100, }; } private kellyFraction(params: PositionSizingParams): PositionSize { const { capital, entryPrice, winRate, avgWinLoss, maxRiskPercent } = params; if (!winRate || !avgWinLoss) { return this.fixedRisk(params); } // Kelly Criterion: f* = (bp - q) / b // b = ratio win/loss, p = win rate, q = 1 - p const b = avgWinLoss; const p = winRate; const q = 1 - p; let kellyPercent = (b * p - q) / b; kellyPercent = Math.max(0, Math.min(kellyPercent, maxRiskPercent)); kellyPercent *= 0.25; // Usar solo 25% del Kelly (half-Kelly) const allocation = capital * kellyPercent; const shares = Math.floor(allocation / entryPrice); return { shares, totalValue: shares * entryPrice, riskAmount: allocation * 0.5, // Estimado percentOfCapital: kellyPercent * 100, }; } } ``` ### 4. Risk Calculator ```typescript // src/modules/copilot/strategy/risk-calculator.ts interface RiskMetrics { riskAmount: number; riskPercent: number; rewardAmount: number; rewardPercent: number; riskRewardRatio: number; breakEvenWinRate: number; expectedValue: number; } interface StopLossParams { method: 'percent' | 'atr_multiple' | 'swing_low' | 'support_level'; value: number; entryPrice: number; atr?: number; swingLow?: number; supportLevel?: number; } export class RiskCalculator { calculateStopLoss(params: StopLossParams): number { const { method, value, entryPrice, atr, swingLow, supportLevel } = params; switch (method) { case 'percent': return entryPrice * (1 - value); case 'atr_multiple': if (!atr) throw new Error('ATR required for atr_multiple method'); return entryPrice - (atr * value); case 'swing_low': if (!swingLow) throw new Error('Swing low required'); return swingLow * (1 - value); // value es buffer % case 'support_level': if (!supportLevel) throw new Error('Support level required'); return supportLevel * (1 - value); default: return entryPrice * 0.95; // Default 5% } } calculateTakeProfit( entryPrice: number, stopLoss: number, riskRewardRatio: number, ): number { const riskPerShare = entryPrice - stopLoss; const rewardPerShare = riskPerShare * riskRewardRatio; return entryPrice + rewardPerShare; } calculateMetrics( entryPrice: number, stopLoss: number, takeProfit: number, positionSize: number, estimatedWinRate?: number, ): RiskMetrics { const riskAmount = (entryPrice - stopLoss) * positionSize; const rewardAmount = (takeProfit - entryPrice) * positionSize; const riskRewardRatio = rewardAmount / riskAmount; const breakEvenWinRate = 1 / (1 + riskRewardRatio); let expectedValue = 0; if (estimatedWinRate) { expectedValue = (estimatedWinRate * rewardAmount) - ((1 - estimatedWinRate) * riskAmount); } return { riskAmount, riskPercent: (riskAmount / (entryPrice * positionSize)) * 100, rewardAmount, rewardPercent: (rewardAmount / (entryPrice * positionSize)) * 100, riskRewardRatio, breakEvenWinRate, expectedValue, }; } } ``` ### 5. Strategy Generator ```typescript // src/modules/copilot/strategy/strategy-generator.ts interface GenerateStrategyParams { symbol: string; userProfile: UserProfile; userPlan: 'free' | 'pro' | 'premium'; marketData: MarketData; mlSignal?: MLSignal; preferredStyle?: 'day' | 'swing' | 'position'; } interface GeneratedStrategy { template: StrategyTemplate; symbol: string; entry: { price: number; condition: string; confidence: number; }; stopLoss: { price: number; percent: number; method: string; }; takeProfit: { price: number; percent: number; riskRewardRatio: number; }; positionSize: PositionSize; riskMetrics: RiskMetrics; explanation: string; invalidationConditions: string[]; disclaimer: string; } @Injectable() export class StrategyGenerator { constructor( private readonly profileAnalyzer: ProfileAnalyzer, private readonly positionSizer: PositionSizer, private readonly riskCalculator: RiskCalculator, private readonly marketDataService: MarketDataService, private readonly mlService: MLSignalService, ) {} async generate(params: GenerateStrategyParams): Promise { const { symbol, userProfile, userPlan, marketData, mlSignal } = params; // 1. Get profile constraints const constraints = this.profileAnalyzer.getConstraints(userProfile); // 2. Filter applicable templates const applicableTemplates = this.filterTemplates( STRATEGY_TEMPLATES, constraints, userPlan, params.preferredStyle, ); // 3. Score and select best template const scoredTemplates = await this.scoreTemplates( applicableTemplates, marketData, mlSignal, ); const selectedTemplate = scoredTemplates[0].template; // 4. Calculate entry/exit levels const { entry, stopLoss, takeProfit } = await this.calculateLevels( selectedTemplate, symbol, marketData, ); // 5. Calculate position size const positionSize = this.positionSizer.calculate({ capital: userProfile.capital, entryPrice: entry.price, stopLossPrice: stopLoss.price, maxRiskPercent: constraints.maxRiskPerTrade, method: selectedTemplate.riskManagement.positionSizing, volatility: marketData.atr, }); // 6. Calculate risk metrics const riskMetrics = this.riskCalculator.calculateMetrics( entry.price, stopLoss.price, takeProfit.price, positionSize.shares, mlSignal?.confidence, ); // 7. Generate explanation const explanation = this.generateExplanation( selectedTemplate, entry, stopLoss, takeProfit, marketData, mlSignal, ); return { template: selectedTemplate, symbol, entry, stopLoss, takeProfit, positionSize, riskMetrics, explanation, invalidationConditions: this.getInvalidationConditions(selectedTemplate), disclaimer: this.getDisclaimer(), }; } private filterTemplates( templates: StrategyTemplate[], constraints: ProfileConstraints, userPlan: string, preferredStyle?: string, ): StrategyTemplate[] { return templates.filter(t => { // Check if strategy is allowed if (!constraints.allowedStrategies.includes(t.id)) return false; // Check ML requirement if (t.requiredIndicators.includes('ML_SIGNAL') && userPlan === 'free') { return false; } // Check style preference if (preferredStyle && t.style !== preferredStyle) return false; return true; }); } private async scoreTemplates( templates: StrategyTemplate[], marketData: MarketData, mlSignal?: MLSignal, ): Promise> { const scored = templates.map(template => { let score = 50; // Base score // Score based on market conditions if (marketData.trend === 'bullish' && template.id.includes('momentum')) { score += 20; } if (marketData.volatility > 0.03 && template.id.includes('breakout')) { score += 15; } // Score based on ML alignment if (mlSignal && template.requiredIndicators.includes('ML_SIGNAL')) { if (mlSignal.confidence > 0.7) { score += 25; } else if (mlSignal.confidence > 0.5) { score += 10; } } return { template, score }; }); return scored.sort((a, b) => b.score - a.score); } private generateExplanation( template: StrategyTemplate, entry: any, stopLoss: any, takeProfit: any, marketData: MarketData, mlSignal?: MLSignal, ): string { let explanation = `La estrategia "${template.name}" es adecuada para las condiciones actuales.\n\n`; explanation += `**Por qué esta estrategia:** ${template.description}\n\n`; explanation += `**Entrada en $${entry.price.toFixed(2)}:** ${entry.condition}\n\n`; explanation += `**Stop Loss en $${stopLoss.price.toFixed(2)} (${stopLoss.percent.toFixed(1)}%):** `; explanation += `Usando método ${stopLoss.method} para proteger el capital.\n\n`; explanation += `**Take Profit en $${takeProfit.price.toFixed(2)} (R:R ${takeProfit.riskRewardRatio.toFixed(1)}:1):** `; explanation += `Objetivo basado en resistencia/proyección técnica.\n\n`; if (mlSignal) { explanation += `**Confirmación ML:** El modelo predice movimiento ${mlSignal.prediction} `; explanation += `con ${(mlSignal.confidence * 100).toFixed(0)}% de confianza.\n\n`; } return explanation; } private getInvalidationConditions(template: StrategyTemplate): string[] { return [ `El precio rompe el nivel de stop loss`, `Cambio de tendencia en timeframe mayor`, `Volumen anormalmente bajo en breakout`, `Noticia de alto impacto contradictoria`, ]; } private getDisclaimer(): string { return `⚠️ Esta sugerencia es informativa y no constituye asesoría financiera. ` + `El trading implica riesgos significativos. Opera bajo tu propio criterio y riesgo. ` + `Nunca inviertas más de lo que puedes permitirte perder.`; } } ``` --- ## Integración con LLM Agent ```typescript // Tool definition para el agente const generateStrategyTool = { type: 'function', function: { name: 'generate_strategy', description: 'Genera una estrategia de trading personalizada para un símbolo', parameters: { type: 'object', properties: { symbol: { type: 'string', description: 'Símbolo del activo', }, style: { type: 'string', enum: ['day', 'swing', 'position'], description: 'Estilo de trading preferido (opcional)', }, }, required: ['symbol'], }, }, }; // Implementación del tool async function generateStrategy( symbol: string, userId: string, userPlan: string, style?: string, ): Promise { const userProfile = await userService.getProfile(userId); const marketData = await marketDataService.getFullContext(symbol); const mlSignal = userPlan !== 'free' ? await mlService.getPrediction(symbol) : undefined; const strategy = await strategyGenerator.generate({ symbol, userProfile, userPlan, marketData, mlSignal, preferredStyle: style, }); return strategy; } ``` --- ## Dependencias ### Módulos Internos - MarketDataService (OQI-003) - MLSignalService (OQI-006) - UserService (OQI-001) ### Bibliotecas - technicalindicators (cálculo de indicadores) - decimal.js (precisión en cálculos financieros) --- ## Referencias - [RF-LLM-003: Sugerencias de Estrategias](../requerimientos/RF-LLM-003-strategy-suggestions.md) - [ET-LLM-002: Agente de Análisis](./ET-LLM-002-agente-analisis.md) --- *Especificación técnica - Sistema NEXUS* *Trading Platform*