FASE 1 - DDL-Backend Coherence (continued): - market-data.types.ts: Updated TickerRow, added Ohlcv5mRow, Ohlcv15mRow, OhlcvStagingRow - llm.types.ts: Updated UserPreferences, UserMemory, Embedding + 3 Row types - financial.types.ts: +6 types (Invoice, WalletAuditLog, etc.) - entity.types.ts (trading): +5 types (Symbol, TradingBot, etc.) FASE 2 - Backend-Frontend Coherence (continued): - llmStore.ts: New Zustand store with session lifecycle management - riskStore.ts: New Zustand store for risk assessment - risk.service.ts: New service with 8 functions - currency.service.ts: New service with 5 functions FASE 3 - Documentation: - OQI-007: Updated to 100% (7 ET, 11 US, 6 RF) - OQI-008: Added ET-PFM-010-architecture.md, ET-PFM-011-goals-system.md - Updated all _MAP.md and README.md indexes Build validation: Backend tsc PASSED, Frontend Vite PASSED Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
875 lines
23 KiB
Markdown
875 lines
23 KiB
Markdown
---
|
||
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<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
|
||
|
||
```typescript
|
||
@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
|
||
|
||
```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<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
|
||
|
||
```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*
|