| id |
title |
type |
status |
priority |
epic |
project |
version |
created_date |
updated_date |
| ET-PFM-011 |
Sistema de Goals (Metas de Inversion) |
Technical Specification |
Done |
Alta |
OQI-008 |
trading-platform |
1.0.0 |
2026-01-28 |
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
@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
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
@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
@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
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
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
@Injectable()
export class GoalService {
constructor(
@InjectRepository(InvestmentGoal)
private goalRepo: Repository<InvestmentGoal>,
@InjectRepository(GoalContribution)
private contributionRepo: Repository<GoalContribution>,
private projectionService: GoalProjectionService,
private eventEmitter: EventEmitter2,
) {}
async createGoal(dto: CreateGoalDto): Promise<InvestmentGoal> {
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<GoalContribution> {
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<InvestmentGoal[]> {
return this.goalRepo.find({
where: { userId, isActive: true },
relations: ['contributions', 'milestones'],
order: { priority: 'ASC', targetDate: 'ASC' },
});
}
async getGoalProgress(goalId: string): Promise<GoalProgressDto> {
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
@Injectable()
export class GoalProjectionService {
async calculateProjection(goal: InvestmentGoal): Promise<GoalProjection> {
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<MonteCarloResult> {
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
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
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
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
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
const GOAL_DEFAULTS: Record<GoalType, GoalDefaults> = {
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
interface GoalAlertSettings {
goalId: string;
milestoneAlerts: boolean;
statusChangeAlerts: boolean;
contributionReminders: boolean;
reminderDayOfMonth: number; // 1-28
channels: ('push' | 'email' | 'sms')[];
}
10. Referencias
Especificacion tecnica - Sistema NEXUS