--- id: "ET-PFM-007" title: "Motor de Metas de Inversión" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-008" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-PFM-007: Motor de Metas de Inversión **Épica:** OQI-008 - Portfolio Manager **Versión:** 1.0 **Fecha:** 2025-12-05 **Estado:** Planificado --- ## Modelo de Datos ```typescript @Entity('investment_goals') export class InvestmentGoal { @PrimaryGeneratedColumn('uuid') id: string; @Column({ name: 'user_id' }) userId: string; @Column({ length: 100 }) name: string; @Column({ type: 'enum', enum: ['retirement', 'home', 'education', 'emergency', 'custom'], }) type: GoalType; @Column('decimal', { precision: 18, scale: 2, name: 'target_amount' }) targetAmount: number; @Column('decimal', { precision: 18, scale: 2, name: 'current_amount' }) currentAmount: number; @Column('decimal', { precision: 18, scale: 2, name: 'monthly_contribution' }) monthlyContribution: number; @Column({ name: 'target_date' }) targetDate: Date; @Column('decimal', { precision: 5, scale: 2, name: 'expected_return' }) expectedReturn: number; @Column({ type: 'enum', enum: ['on_track', 'ahead', 'behind'], default: 'on_track', }) status: GoalStatus; @Column({ name: 'linked_account_id', nullable: true }) linkedAccountId: string; @CreateDateColumn({ name: 'created_at' }) createdAt: Date; @UpdateDateColumn({ name: 'updated_at' }) updatedAt: Date; } ``` --- ## Servicio de Proyección ```typescript @Injectable() export class GoalProjectionService { calculateProjection(goal: InvestmentGoal): GoalProjection { const monthsRemaining = this.getMonthsRemaining(goal.targetDate); const monthlyReturn = goal.expectedReturn / 12 / 100; // Future Value con contribuciones mensuales // FV = PV(1+r)^n + PMT × ((1+r)^n - 1) / r const futureValue = goal.currentAmount * Math.pow(1 + monthlyReturn, monthsRemaining) + goal.monthlyContribution * ((Math.pow(1 + monthlyReturn, monthsRemaining) - 1) / monthlyReturn); const progressPercent = (goal.currentAmount / goal.targetAmount) * 100; const projectedPercent = (futureValue / goal.targetAmount) * 100; return { currentProgress: progressPercent, projectedValue: futureValue, projectedProgress: projectedPercent, shortfall: Math.max(0, goal.targetAmount - futureValue), status: this.determineStatus(projectedPercent), scenarios: this.calculateScenarios(goal, monthsRemaining), }; } 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), }; } calculateRequiredContribution(goal: InvestmentGoal): number { const months = this.getMonthsRemaining(goal.targetDate); const monthlyReturn = goal.expectedReturn / 12 / 100; // PMT = (FV - PV(1+r)^n) × r / ((1+r)^n - 1) const fvOfCurrent = goal.currentAmount * Math.pow(1 + monthlyReturn, months); const remaining = goal.targetAmount - fvOfCurrent; const factor = (Math.pow(1 + monthlyReturn, months) - 1) / monthlyReturn; return remaining / factor; } } ``` --- ## API Endpoints | Method | Endpoint | Descripción | |--------|----------|-------------| | GET | `/api/goals` | Lista de metas | | POST | `/api/goals` | Crear meta | | GET | `/api/goals/:id` | Detalle de meta | | PUT | `/api/goals/:id` | Actualizar meta | | DELETE | `/api/goals/:id` | Eliminar meta | | GET | `/api/goals/:id/projection` | Proyección de meta | | POST | `/api/goals/:id/simulate` | Simular escenarios | --- ## Referencias - [RF-PFM-007: Metas de Inversión](../requerimientos/RF-PFM-007-metas-inversión.md) --- *Especificación técnica - Sistema NEXUS*