--- id: "ET-PFM-011" title: "Sistema de Goals (Metas de Inversion)" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-008" project: "trading-platform" version: "1.0.0" created_date: "2026-01-28" updated_date: "2026-01-28" --- # ET-PFM-011: Sistema de Goals (Metas de Inversion) **Epica:** OQI-008 - Portfolio Manager **Version:** 1.0 **Fecha:** 2026-01-28 **Estado:** Planificado --- ## 1. Vision General El Sistema de Goals permite a los usuarios definir, seguir y proyectar metas financieras a largo plazo como retiro, compra de casa, educacion de hijos, etc. Integra simulaciones Monte Carlo para proyectar probabilidades de exito. ### 1.1 Tipos de Metas Soportadas | Tipo | Icono | Descripcion | Horizonte Tipico | |------|-------|-------------|------------------| | `retirement` | 🏖️ | Retiro/Jubilacion | 10-30 anos | | `home` | 🏠 | Compra de casa/enganche | 2-10 anos | | `education` | 🎓 | Educacion de hijos | 5-18 anos | | `emergency` | 🚨 | Fondo de emergencia | 1-2 anos | | `travel` | ✈️ | Viaje mayor | 1-5 anos | | `vehicle` | 🚗 | Compra de vehiculo | 1-5 anos | | `wedding` | 💍 | Boda/evento | 1-3 anos | | `custom` | 🎯 | Meta personalizada | Variable | --- ## 2. Modelo de Datos ### 2.1 Entity: InvestmentGoal ```typescript @Entity('investment_goals') export class InvestmentGoal { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'user_id' }) @Index() userId: string; @Column({ length: 100 }) name: string; @Column({ length: 500, nullable: true }) description: string; @Column({ type: 'enum', enum: GoalType, default: GoalType.CUSTOM, }) type: GoalType; @Column('decimal', { precision: 18, scale: 2, name: 'target_amount' }) targetAmount: number; @Column('decimal', { precision: 18, scale: 2, name: 'current_amount', default: 0 }) currentAmount: number; @Column('decimal', { precision: 18, scale: 2, name: 'initial_amount', default: 0 }) initialAmount: number; @Column('decimal', { precision: 18, scale: 2, name: 'monthly_contribution', default: 0 }) monthlyContribution: number; @Column({ name: 'target_date', type: 'date' }) targetDate: Date; @Column('decimal', { precision: 5, scale: 2, name: 'expected_return', default: 7.0 }) expectedReturn: number; // Annual return % @Column('decimal', { precision: 5, scale: 2, name: 'expected_volatility', default: 15.0 }) expectedVolatility: number; // Annual volatility % @Column({ type: 'enum', enum: GoalStatus, default: GoalStatus.ON_TRACK, }) status: GoalStatus; @Column({ type: 'enum', enum: GoalPriority, default: GoalPriority.MEDIUM, }) priority: GoalPriority; @Column({ name: 'linked_account_id', type: 'uuid', nullable: true }) linkedAccountId: string | null; @Column({ name: 'is_active', default: true }) isActive: boolean; @Column('jsonb', { name: 'risk_profile', nullable: true }) riskProfile: GoalRiskProfile | null; @Column('jsonb', { name: 'last_projection', nullable: true }) lastProjection: GoalProjection | null; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; @OneToMany(() => GoalContribution, (c) => c.goal) contributions: GoalContribution[]; @OneToMany(() => GoalMilestone, (m) => m.goal) milestones: GoalMilestone[]; } ``` ### 2.2 Enums ```typescript enum GoalType { RETIREMENT = 'retirement', HOME = 'home', EDUCATION = 'education', EMERGENCY = 'emergency', TRAVEL = 'travel', VEHICLE = 'vehicle', WEDDING = 'wedding', CUSTOM = 'custom', } enum GoalStatus { ON_TRACK = 'on_track', AHEAD = 'ahead', BEHIND = 'behind', AT_RISK = 'at_risk', ACHIEVED = 'achieved', PAUSED = 'paused', } enum GoalPriority { HIGH = 'high', MEDIUM = 'medium', LOW = 'low', } ``` ### 2.3 Entity: GoalContribution ```typescript @Entity('goal_contributions') export class GoalContribution { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'goal_id' }) goalId: string; @ManyToOne(() => InvestmentGoal, (g) => g.contributions) @JoinColumn({ name: 'goal_id' }) goal: InvestmentGoal; @Column('decimal', { precision: 18, scale: 2 }) amount: number; @Column({ type: 'enum', enum: ContributionType, default: ContributionType.DEPOSIT, }) type: ContributionType; @Column({ nullable: true }) source: string; // 'manual', 'automatic', 'dividend', 'interest' @Column({ type: 'text', nullable: true }) notes: string; @Column({ name: 'transaction_id', nullable: true }) transactionId: string; @CreateDateColumn({ name: 'contributed_at' }) contributedAt: Date; } enum ContributionType { DEPOSIT = 'deposit', WITHDRAWAL = 'withdrawal', INTEREST = 'interest', DIVIDEND = 'dividend', ADJUSTMENT = 'adjustment', } ``` ### 2.4 Entity: GoalMilestone ```typescript @Entity('goal_milestones') export class GoalMilestone { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'goal_id' }) goalId: string; @ManyToOne(() => InvestmentGoal, (g) => g.milestones) @JoinColumn({ name: 'goal_id' }) goal: InvestmentGoal; @Column({ length: 100 }) name: string; @Column('decimal', { precision: 18, scale: 2, name: 'target_amount' }) targetAmount: number; @Column('decimal', { precision: 5, scale: 2, name: 'target_percentage' }) targetPercentage: number; // % of goal @Column({ name: 'target_date', type: 'date', nullable: true }) targetDate: Date | null; @Column({ name: 'achieved_at', type: 'timestamp', nullable: true }) achievedAt: Date | null; @Column({ default: false }) isAchieved: boolean; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; } ``` --- ## 3. Interfaces de Proyeccion ### 3.1 GoalProjection ```typescript interface GoalProjection { // Metadata goalId: string; calculatedAt: Date; // Current State currentAmount: number; targetAmount: number; progressPercent: number; monthsRemaining: number; // Projections projectedValue: number; projectedProgress: number; shortfall: number; // Probability Analysis probabilityOfSuccess: number; confidenceInterval: { low: number; // P10 mid: number; // P50 (median) high: number; // P90 }; // Scenarios scenarios: GoalScenarios; // Recommendations requiredMonthlyContribution: number; suggestedActions: string[]; } interface GoalScenarios { optimistic: ScenarioResult; base: ScenarioResult; pessimistic: ScenarioResult; } interface ScenarioResult { projectedValue: number; endDate: Date; returnRate: number; probabilityOfSuccess: number; } ``` ### 3.2 MonteCarloParams ```typescript interface MonteCarloParams { initialAmount: number; monthlyContribution: number; expectedReturn: number; // Annual, e.g., 0.07 for 7% volatility: number; // Annual, e.g., 0.15 for 15% months: number; numSimulations: number; // Default: 10000 targetAmount: number; } interface MonteCarloResult { simulations: number[][]; // [sim][month] finalValues: number[]; // Final value per simulation percentiles: { p10: number; p25: number; p50: number; p75: number; p90: number; p95: number; }; probabilityOfSuccess: number; averageFinalValue: number; medianFinalValue: number; } ``` --- ## 4. Servicio de Goals ### 4.1 GoalService ```typescript @Injectable() export class GoalService { constructor( @InjectRepository(InvestmentGoal) private goalRepo: Repository, @InjectRepository(GoalContribution) private contributionRepo: Repository, private projectionService: GoalProjectionService, private eventEmitter: EventEmitter2, ) {} async createGoal(dto: CreateGoalDto): Promise { const goal = this.goalRepo.create({ ...dto, status: GoalStatus.ON_TRACK, currentAmount: dto.initialAmount || 0, }); // Create default milestones goal.milestones = this.createDefaultMilestones(goal); const saved = await this.goalRepo.save(goal); // Calculate initial projection const projection = await this.projectionService.calculateProjection(saved); saved.lastProjection = projection; saved.status = this.determineStatus(projection); await this.goalRepo.save(saved); this.eventEmitter.emit('goal.created', saved); return saved; } async addContribution( goalId: string, dto: AddContributionDto ): Promise { const goal = await this.getGoalById(goalId); const contribution = this.contributionRepo.create({ goalId, ...dto, }); await this.contributionRepo.save(contribution); // Update goal current amount const delta = dto.type === ContributionType.WITHDRAWAL ? -dto.amount : dto.amount; goal.currentAmount = Number(goal.currentAmount) + delta; // Recalculate projection const projection = await this.projectionService.calculateProjection(goal); goal.lastProjection = projection; goal.status = this.determineStatus(projection); // Check milestones await this.checkMilestones(goal); await this.goalRepo.save(goal); this.eventEmitter.emit('goal.contribution.added', { goal, contribution }); return contribution; } async getGoalsByUser(userId: string): Promise { return this.goalRepo.find({ where: { userId, isActive: true }, relations: ['contributions', 'milestones'], order: { priority: 'ASC', targetDate: 'ASC' }, }); } async getGoalProgress(goalId: string): Promise { const goal = await this.getGoalById(goalId); const projection = await this.projectionService.calculateProjection(goal); const contributions = await this.contributionRepo.find({ where: { goalId }, order: { contributedAt: 'DESC' }, take: 12, // Last 12 contributions }); return { goal, projection, contributions, milestones: goal.milestones.map(m => ({ ...m, progress: (goal.currentAmount / m.targetAmount) * 100, })), }; } private createDefaultMilestones(goal: InvestmentGoal): GoalMilestone[] { return [ { name: '25% alcanzado', targetPercentage: 25, targetAmount: goal.targetAmount * 0.25 }, { name: '50% alcanzado', targetPercentage: 50, targetAmount: goal.targetAmount * 0.50 }, { name: '75% alcanzado', targetPercentage: 75, targetAmount: goal.targetAmount * 0.75 }, { name: 'Meta completada', targetPercentage: 100, targetAmount: goal.targetAmount }, ].map(m => this.milestoneRepo.create({ ...m, goalId: goal.id })); } private determineStatus(projection: GoalProjection): GoalStatus { if (projection.probabilityOfSuccess >= 95) return GoalStatus.AHEAD; if (projection.probabilityOfSuccess >= 75) return GoalStatus.ON_TRACK; if (projection.probabilityOfSuccess >= 50) return GoalStatus.BEHIND; return GoalStatus.AT_RISK; } } ``` --- ## 5. Servicio de Proyeccion ### 5.1 GoalProjectionService ```typescript @Injectable() export class GoalProjectionService { async calculateProjection(goal: InvestmentGoal): Promise { const monthsRemaining = this.getMonthsRemaining(goal.targetDate); // Deterministic projection (Future Value formula) const projectedValue = this.calculateFutureValue( goal.currentAmount, goal.monthlyContribution, goal.expectedReturn / 100 / 12, // Monthly rate monthsRemaining ); // Monte Carlo simulation const mcResult = await this.runMonteCarloSimulation({ initialAmount: Number(goal.currentAmount), monthlyContribution: Number(goal.monthlyContribution), expectedReturn: goal.expectedReturn / 100, volatility: goal.expectedVolatility / 100, months: monthsRemaining, numSimulations: 10000, targetAmount: Number(goal.targetAmount), }); const progressPercent = (goal.currentAmount / goal.targetAmount) * 100; const projectedProgress = (projectedValue / goal.targetAmount) * 100; return { goalId: goal.id, calculatedAt: new Date(), currentAmount: Number(goal.currentAmount), targetAmount: Number(goal.targetAmount), progressPercent, monthsRemaining, projectedValue, projectedProgress, shortfall: Math.max(0, Number(goal.targetAmount) - projectedValue), probabilityOfSuccess: mcResult.probabilityOfSuccess, confidenceInterval: { low: mcResult.percentiles.p10, mid: mcResult.percentiles.p50, high: mcResult.percentiles.p90, }, scenarios: this.calculateScenarios(goal, monthsRemaining), requiredMonthlyContribution: this.calculateRequiredContribution(goal), suggestedActions: this.generateSuggestions(goal, mcResult.probabilityOfSuccess), }; } /** * Future Value with regular deposits * FV = PV(1+r)^n + PMT × ((1+r)^n - 1) / r */ private calculateFutureValue( presentValue: number, monthlyPayment: number, monthlyRate: number, months: number ): number { if (monthlyRate === 0) { return presentValue + monthlyPayment * months; } const growthFactor = Math.pow(1 + monthlyRate, months); const fvOfPresentValue = presentValue * growthFactor; const fvOfPayments = monthlyPayment * ((growthFactor - 1) / monthlyRate); return fvOfPresentValue + fvOfPayments; } /** * Monte Carlo Simulation * Geometric Brownian Motion: S(t+1) = S(t) * exp((mu - sigma^2/2)*dt + sigma*sqrt(dt)*Z) */ async runMonteCarloSimulation(params: MonteCarloParams): Promise { const { initialAmount, monthlyContribution, expectedReturn, volatility, months, numSimulations, targetAmount } = params; const monthlyReturn = expectedReturn / 12; const monthlyVolatility = volatility / Math.sqrt(12); const dt = 1; // 1 month const simulations: number[][] = []; const finalValues: number[] = []; for (let sim = 0; sim < numSimulations; sim++) { const path: number[] = [initialAmount]; let value = initialAmount; for (let month = 1; month <= months; month++) { // Random normal (Box-Muller transform) const z = this.randomNormal(); // GBM step const drift = (monthlyReturn - 0.5 * monthlyVolatility ** 2) * dt; const diffusion = monthlyVolatility * Math.sqrt(dt) * z; value = value * Math.exp(drift + diffusion) + monthlyContribution; path.push(Math.max(0, value)); // No negative values } simulations.push(path); finalValues.push(path[path.length - 1]); } // Sort for percentile calculation const sorted = [...finalValues].sort((a, b) => a - b); const percentile = (p: number) => sorted[Math.floor(p * numSimulations)]; const successCount = finalValues.filter(v => v >= targetAmount).length; return { simulations, finalValues, percentiles: { p10: percentile(0.10), p25: percentile(0.25), p50: percentile(0.50), p75: percentile(0.75), p90: percentile(0.90), p95: percentile(0.95), }, probabilityOfSuccess: (successCount / numSimulations) * 100, averageFinalValue: finalValues.reduce((a, b) => a + b, 0) / numSimulations, medianFinalValue: percentile(0.50), }; } /** * Box-Muller transform for normal distribution */ private randomNormal(): number { const u1 = Math.random(); const u2 = Math.random(); return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); } /** * Calculate required monthly contribution to reach target * PMT = (FV - PV(1+r)^n) × r / ((1+r)^n - 1) */ calculateRequiredContribution(goal: InvestmentGoal): number { const months = this.getMonthsRemaining(goal.targetDate); const monthlyRate = goal.expectedReturn / 100 / 12; if (months <= 0) return 0; if (monthlyRate === 0) { return (Number(goal.targetAmount) - Number(goal.currentAmount)) / months; } const growthFactor = Math.pow(1 + monthlyRate, months); const fvOfCurrent = Number(goal.currentAmount) * growthFactor; const remaining = Number(goal.targetAmount) - fvOfCurrent; if (remaining <= 0) return 0; const factor = (growthFactor - 1) / monthlyRate; return remaining / factor; } private calculateScenarios(goal: InvestmentGoal, months: number): GoalScenarios { return { optimistic: this.projectWithReturn(goal, months, goal.expectedReturn + 3), base: this.projectWithReturn(goal, months, goal.expectedReturn), pessimistic: this.projectWithReturn(goal, months, goal.expectedReturn - 3), }; } private projectWithReturn(goal: InvestmentGoal, months: number, annualReturn: number): ScenarioResult { const monthlyRate = annualReturn / 100 / 12; const projectedValue = this.calculateFutureValue( Number(goal.currentAmount), Number(goal.monthlyContribution), monthlyRate, months ); return { projectedValue, endDate: goal.targetDate, returnRate: annualReturn, probabilityOfSuccess: projectedValue >= Number(goal.targetAmount) ? 100 : (projectedValue / Number(goal.targetAmount)) * 100, }; } private generateSuggestions(goal: InvestmentGoal, probability: number): string[] { const suggestions: string[] = []; if (probability < 50) { const required = this.calculateRequiredContribution(goal); const increase = required - Number(goal.monthlyContribution); if (increase > 0) { suggestions.push(`Incrementar aportacion mensual en $${increase.toFixed(0)} para alcanzar meta`); } suggestions.push('Considerar extender la fecha objetivo'); suggestions.push('Evaluar perfil de riesgo para mayor rendimiento esperado'); } else if (probability < 75) { suggestions.push('Meta en riesgo. Considerar aumentar aportaciones'); } else if (probability < 95) { suggestions.push('Meta en buen camino. Mantener aportaciones actuales'); } else { suggestions.push('Excelente progreso! Meta muy probable de alcanzar'); } return suggestions; } private getMonthsRemaining(targetDate: Date): number { const now = new Date(); const target = new Date(targetDate); return Math.max(0, (target.getFullYear() - now.getFullYear()) * 12 + (target.getMonth() - now.getMonth())); } } ``` --- ## 6. API Endpoints ### 6.1 Goals CRUD | Method | Endpoint | Descripcion | |--------|----------|-------------| | GET | `/api/goals` | Lista de metas del usuario | | POST | `/api/goals` | Crear nueva meta | | GET | `/api/goals/:id` | Detalle de meta | | PUT | `/api/goals/:id` | Actualizar meta | | DELETE | `/api/goals/:id` | Eliminar meta | | PATCH | `/api/goals/:id/pause` | Pausar meta | | PATCH | `/api/goals/:id/resume` | Reanudar meta | ### 6.2 Contributions | Method | Endpoint | Descripcion | |--------|----------|-------------| | GET | `/api/goals/:id/contributions` | Historial de contribuciones | | POST | `/api/goals/:id/contributions` | Agregar contribucion | ### 6.3 Projections & Analytics | Method | Endpoint | Descripcion | |--------|----------|-------------| | GET | `/api/goals/:id/progress` | Progreso con proyeccion | | GET | `/api/goals/:id/projection` | Proyeccion Monte Carlo | | POST | `/api/goals/:id/simulate` | Simular escenarios custom | | GET | `/api/goals/:id/milestones` | Estado de milestones | --- ## 7. Componentes Frontend ### 7.1 GoalDashboard ```typescript interface GoalDashboardProps { userId: string; } // Componentes hijos: // - GoalSummaryCard: Resumen de todas las metas // - GoalList: Lista de metas con progreso // - AddGoalButton: Crear nueva meta ``` ### 7.2 GoalCard ```typescript interface GoalCardProps { goal: InvestmentGoal; onEdit: () => void; onContribute: () => void; } // Muestra: // - Nombre e icono del tipo // - Barra de progreso visual // - Monto actual / objetivo // - Fecha objetivo // - Estado (on_track, behind, etc.) // - Probabilidad de exito ``` ### 7.3 GoalProjectionChart ```typescript interface GoalProjectionChartProps { projection: GoalProjection; height?: number; } // Grafico que muestra: // - Linea de progreso actual // - Bandas de confianza (P10, P50, P90) // - Linea objetivo // - Fecha objetivo ``` ### 7.4 GoalWizard ```typescript interface GoalWizardProps { onComplete: (goal: InvestmentGoal) => void; } // Pasos: // 1. Seleccionar tipo de meta // 2. Definir nombre y monto objetivo // 3. Fecha objetivo // 4. Aportacion mensual inicial // 5. Perfil de riesgo (conservador/moderado/agresivo) // 6. Confirmar y ver proyeccion inicial ``` --- ## 8. Configuraciones por Tipo de Meta ### 8.1 Defaults por Tipo ```typescript const GOAL_DEFAULTS: Record = { retirement: { expectedReturn: 7.0, expectedVolatility: 12.0, suggestedHorizon: '20+ years', riskProfile: 'moderate', }, home: { expectedReturn: 5.0, expectedVolatility: 8.0, suggestedHorizon: '3-7 years', riskProfile: 'conservative', }, education: { expectedReturn: 6.0, expectedVolatility: 10.0, suggestedHorizon: '5-18 years', riskProfile: 'moderate', }, emergency: { expectedReturn: 2.0, expectedVolatility: 2.0, suggestedHorizon: '6-12 months', riskProfile: 'conservative', }, travel: { expectedReturn: 4.0, expectedVolatility: 5.0, suggestedHorizon: '1-3 years', riskProfile: 'conservative', }, vehicle: { expectedReturn: 4.0, expectedVolatility: 6.0, suggestedHorizon: '1-5 years', riskProfile: 'conservative', }, wedding: { expectedReturn: 3.0, expectedVolatility: 4.0, suggestedHorizon: '1-3 years', riskProfile: 'conservative', }, custom: { expectedReturn: 5.0, expectedVolatility: 10.0, suggestedHorizon: 'Variable', riskProfile: 'moderate', }, }; ``` --- ## 9. Notificaciones y Alertas ### 9.1 Eventos de Notificacion | Evento | Trigger | Canal | |--------|---------|-------| | `goal.milestone.reached` | Progreso >= milestone % | Push, Email | | `goal.status.changed` | Status cambia a behind/at_risk | Push, Email | | `goal.achieved` | Monto actual >= objetivo | Push, Email, Celebracion UI | | `goal.contribution.reminder` | Dia de aportacion programada | Push | | `goal.review.monthly` | Primer dia del mes | Email digest | ### 9.2 Configuracion de Alertas ```typescript interface GoalAlertSettings { goalId: string; milestoneAlerts: boolean; statusChangeAlerts: boolean; contributionReminders: boolean; reminderDayOfMonth: number; // 1-28 channels: ('push' | 'email' | 'sms')[]; } ``` --- ## 10. Referencias - [ET-PFM-007: Motor de Metas](./ET-PFM-007-motor-metas.md) - [RF-PFM-007: Metas de Inversion](../requerimientos/RF-PFM-007-metas-inversion.md) - [US-PFM-012: Reporte Fiscal](../historias-usuario/US-PFM-012-reporte-fiscal.md) - [TRACEABILITY.yml](../implementacion/TRACEABILITY.yml) --- *Especificacion tecnica - Sistema NEXUS*