ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
26 KiB
26 KiB
| id | title | type | status | priority | epic | project | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|---|
| ET-LLM-003 | Motor de Generación de Estrategias | Technical Specification | Done | Alta | OQI-007 | trading-platform | 1.0.0 | 2025-12-05 | 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
// 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
// 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
// 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
// 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
// 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<GeneratedStrategy> {
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<Array<{ template: StrategyTemplate; score: number }>> {
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
// 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<GeneratedStrategy> {
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
Especificación técnica - Sistema NEXUS Trading Platform