Skip to main content

Projected Value Business Logic

Overview

Projected Value represents the target amount a goal should reach by a given date based on:

  • Regular monthly contributions
  • Expected annual return rate
  • Time elapsed from goal start date

Key Principle: Initial contributions are excluded from projection. Projected value is calculated purely from monthly investment contributions.

Compounding Approaches

The system supports two projection methods for different use cases:

AspectMonthly CompoundingDaily Compounding
When UsedChart projections, goal form calculationsCurrent-date on-track determination
GranularityMonth-end dates onlyAny specific date
Precision~0.13% lower~0.13% higher
FunctioncalculateProjectedValue()calculateProjectedValueByDate()
ConsistencyMatches goal form setupMore realistic real-world

Core Formula (Monthly Compounding)

FV = PV × (1 + r)^n + PMT × [((1 + r)^n - 1) / r]

Where:

  • FV = Future Value (Projected Value)
  • PV = Initial Contributions (starting allocation)
  • PMT = Monthly Investment Amount
  • r = Monthly Return Rate (annual rate / 12)
  • n = Number of months from goal start date

Why Monthly Compounding?

Monthly compounding is used for consistency with the goal form calculation. When a user creates a goal:

  1. Goal Form calculates required monthly investment using monthly compounding
  2. Projected Value uses the same monthly compounding formula
  3. Result: Projected Future Value at due date = Target Amount (consistent)

If different compounding methods were used, the projected value would not match the target, causing user confusion.

Why Not Include Initial Contribution?

Initial contributions represent what you've already allocated at goal start. The projection shows what you expect to achieve through ongoing contributions and growth, not what you already have.

Example:

  • Initial allocation: 34,000,000 (locked at goal start)
  • Monthly contribution: 1,000,000
  • Annual return: 7%
  • Months elapsed: 1

Projected should show: ~1,005,833 (from contributions + growth) Not: 34,000,000 + 1,005,833 (mixing allocated with projected)

Alternative Formula: Daily Compounding

For more accurate real-world projections, daily compounding can be used. Unlike monthly compounding, daily compounding calculates projection for any specific date (not just month milestones).

Daily Compounding Formula

FV_contributions = PMT_daily × [((1 + r_daily)^n - 1) / r_daily]

Where:

  • FV_contributions = Future Value of contributions only
  • PMT_daily = Daily Investment Amount (typically back-calculated to reach target)
  • r_daily = Daily Return Rate (annual rate / 100 / 365)
  • n = Number of days from goal start date to target date

Note: The startValue (Initial Principal) is technically excluded from this specific function's output in the current implementation, as the projection line focuses on the growth of systematic investments.

  • r_daily = Daily Return Rate (annual rate / 100 / 365)
  • n = Number of days from goal start date to target date

When to Use Daily Compounding

  • For current-date on-track determination: More precise than monthly
  • For any specific date projection: Can calculate for Dec 15, not just Dec 31
  • For daily charts or real-time tracking: Better granularity

Precision Difference

Daily vs Monthly compounding for 5 years at 8% annual rate:

  • Monthly compounding: (1 + 0.08/12)^60 = 1.4898
  • Daily compounding: (1 + 0.08/365)^1825 = 1.4917

Difference: ~0.13% higher with daily compounding (more realistic)

Calculation Details (Current Implementation)

1. Parameter Conversion

Annual to Monthly Return Rate:

const monthlyRate = annualReturnRate / 100 / 12;

Example: 7% annual = 0.583% monthly (7 / 100 / 12 = 0.00583)

2. Time Calculation

const monthsFromStart = differenceInMonths(today, goalStartDate);

Uses date-fns differenceInMonths for accurate month difference.

3. Compound Factor

const compoundFactor = Math.pow(1 + monthlyRate, monthsFromStart);

This represents: (1 + r)^n

4. Future Value Calculation

If monthly rate is 0:

return monthlyInvestment * monthsFromStart;

Simple accumulation of contributions.

If monthly rate > 0:

const compoundFactor = Math.pow(1 + monthlyRate, monthsFromStart);
return monthlyInvestment * ((compoundFactor - 1) / monthlyRate);

Compound interest formula for regular contributions. Note that detailed implementation excludes startValue (Initial Principal) from this specific calculation to isolate the growth of new contributions.

Implementation Locations

1. goal-utils.ts - calculateProjectedValue()

Purpose: Calculate projected value for any date, used for chart rendering and "On Track" determination. Excludes initial contributions per business logic.

export function calculateProjectedValue(
startValue: number, // NOT USED - kept for backwards compatibility
monthlyInvestment: number,
annualReturnRate: number,
monthsFromStart: number,
): number {
// At goal start date, projected value is 0 (no contributions yet)
if (monthsFromStart <= 0) return 0;

const monthlyRate = annualReturnRate / 100 / 12;

if (monthlyRate === 0) {
// No return: just sum of contributions
return monthlyInvestment * monthsFromStart;
}

// Compound interest only from monthly contributions
const compoundFactor = Math.pow(1 + monthlyRate, monthsFromStart);
const futureContributions = monthlyInvestment * ((compoundFactor - 1) / monthlyRate);

return futureContributions;
}

Called from:

  • use-goal-progress.ts: For "On Track" determination

    const monthsFromStart = getMonthsDiff(goalStartDate, today);
    const projectedValue = calculateProjectedValue(
    0, // Initial contributions excluded from projection
    monthlyInvestment,
    annualReturnRate,
    Math.max(0, monthsFromStart),
    );
  • use-goal-valuation-history.ts: For chart data points

    const monthsFromStart = getMonthsDiff(goalStartDate, date);
    const projected = calculateProjectedValue(
    0, // Initial contributions excluded
    monthlyInvestment,
    annualReturnRate,
    Math.max(0, monthsFromStart),
    );

2. goal-utils.ts - Daily Compounding Functions

Purpose: Provide daily-precision projection calculations for on-track determination and any-date projections.

getDaysDiff(startDate, endDate)

Calculates exact number of days between two dates.

export function getDaysDiff(startDate: Date, endDate: Date): number {
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
}

calculateProjectedValueByDate(startValue, monthlyInvestment, annualReturnRate, startDate, currentDate)

Calculates projected value at any specific date using daily compounding. Note: startValue is passed for API consistency but is currently unused in the internal calculation logic.

// Example: Project value for Dec 15 (not just Dec 31)
const projected = calculateProjectedValueByDate(
startValue: 1000000, // Passed but unused in projection logic
dailyInvestment: 3333, // Daily contribution
annualReturnRate: 7, // 7% annual
startDate: new Date('2025-01-01'),
currentDate: new Date('2025-12-15') // Any date
);

Key Formula:

  • Converts monthly investment to daily: dailyInvestment = monthlyInvestment / 30
  • Daily rate: dailyRate = annualReturnRate / 100 / 365
  • Applies compound interest for exact days elapsed

isGoalOnTrackByDate(currentValue, startValue, monthlyInvestment, annualReturnRate, startDate)

Determines on-track status using daily compounding (more precise than monthly).

const onTrack = isGoalOnTrackByDate(
currentValue: 500000,
startValue: 1000000,
monthlyInvestment: 100000,
annualReturnRate: 7,
startDate: new Date('2025-01-01')
);

4. goal-details-page.tsx

Purpose: Display goal details with on-track status.

Single Source of Truth: Uses goalProgress.isOnTrack from the hook to ensure consistency between Goal Card and Goal Details page.

// Use goalProgress.isOnTrack as single source of truth (same as Goal Card)
const onTrack = goalProgress?.isOnTrack ?? true;

5. use-goal-valuation-history.ts

Purpose: Calculate projected values for all dates in the chart (weeks/months/years/all), used for chart rendering.

Uses monthly compounding for consistency, called for each date interval:

const projected = calculateProjectedValue(
startValue,
monthlyInvestment,
annualReturnRate,
Math.max(0, monthsFromStart),
);

Calculation Scenarios

Scenario 1: No Monthly Investment, No Return

Setup:

  • Monthly investment: 0
  • Annual return rate: 0%
  • Months from start: 1

Calculation:

monthlyInvestment = 0
projectedValue = 0

Result: 0 (no growth without contributions or returns)


Scenario 2: Monthly Investment, No Return

Setup:

  • Monthly investment: 1,000,000
  • Annual return rate: 0%
  • Months from start: 1

Calculation:

projectedValue = 1,000,000 × 1 = 1,000,000

Result: 1,000,000 (purely from contributions)


Scenario 3: Monthly Investment + Return

Setup:

  • Monthly investment: 1,000,000
  • Annual return rate: 7%
  • Months from start: 1

Calculation:

monthlyRate = 7 / 100 / 12 = 0.00583
compoundFactor = (1 + 0.00583)^1 = 1.00583
projectedValue = 1,000,000 × ((1.00583 - 1) / 0.00583)
= 1,000,000 × 1.0
= 1,000,000

Result: 1,000,000 (first month, minimal compound effect)


Scenario 4: 1 Year Projection

Setup:

  • Monthly investment: 1,000,000
  • Annual return rate: 7%
  • Months from start: 12

Calculation:

monthlyRate = 0.00583
compoundFactor = (1.00583)^12 = 1.0723
projectedValue = 1,000,000 × ((1.0723 - 1) / 0.00583)
= 1,000,000 × 12.40
= 12,400,000

Result: 12,400,000 (approximately: 12M from contributions + 400K from growth)


Scenario 5: Multiple Years

Setup:

  • Initial allocation: 34,000,000 (NOT included in projection)
  • Monthly investment: 1,000,000
  • Annual return rate: 7%
  • Months from start: 24 (2 years)

Calculation:

monthlyRate = 0.00583
compoundFactor = (1.00583)^24 = 1.1498
projectedValue = 1,000,000 × ((1.1498 - 1) / 0.00583)
= 1,000,000 × 25.69
= 25,690,000

Result: 25,690,000 (only from monthly contributions, not initial allocation)

Note: The 34M initial allocation affects actual value, not projected value.

On-Track Determination

Logic:

isOnTrack = currentValue >= projectedValue;

Where:

  • currentValue = initialContribution + (accountGrowth × allocatedPercent)
  • projectedValue = PMT × [((1 + r)^n - 1) / r] (using monthly compounding)

Single Source of Truth: The goalProgress.isOnTrack from use-goal-progress.ts hook is used by both Goal Card and Goal Details page to ensure consistency.

Example: On Track

  • Goal start: Jan 1, 2025
  • Today: Feb 1, 2025 (1 month)
  • Initial allocation: 34,000,000
  • Monthly investment: 1,000,000
  • Annual return: 7%
  • Actual account growth: 500,000

Actual value:

currentValue = 34,000,000 + (500,000 × 100%)
= 34,500,000

Projected value:

monthlyRate = 7 / 100 / 12 = 0.00583
compoundFactor = (1.00583)^1 = 1.00583
projectedValue = 1,000,000 × ((1.00583 - 1) / 0.00583)
= 1,000,000

Determination:

34,500,000 >= 1,000,000 → ON TRACK ✓

Example: Off Track

Same setup but:

  • Actual account growth: -500,000 (loss)

Actual value:

currentValue = 34,000,000 + (-500,000 × 100%)
= 33,500,000

Determination:

33,500,000 >= 1,000,000 → ON TRACK ✓ (still on track!)

Example with smaller initial allocation:

Setup:

  • Initial allocation: 1,000,000
  • Monthly investment: 10,000
  • Annual return: 7%
  • Months from start: 1
  • Actual account growth: 0 (no growth)

Actual value:

currentValue = 1,000,000 + (0 × 100%)
= 1,000,000

Projected value:

projectedValue = 10,000 × 1 = 10,000

Determination:

1,000,000 >= 10,000 → ON TRACK ✓

Example: Truly Off Track

Setup:

  • Initial allocation: 100,000 (that was supposed to grow)
  • Monthly investment: 1,000
  • Annual return: 10%
  • Months from start: 12
  • Actual account growth: -50,000 (loss)

Actual value:

currentValue = 100,000 + (-50,000 × 100%)
= 50,000

Projected value:

monthlyRate = 10 / 100 / 12 = 0.00833
compoundFactor = (1.00833)^12 = 1.1047
projectedValue = 1,000 × ((1.1047 - 1) / 0.00833)
= 1,000 × 12.56
= 12,560

Determination:

50,000 >= 12,560 → ON TRACK ✓

(Even with loss, initial allocation keeps it on track)


Example: Truly Off Track (Version 2)

Setup:

  • Initial allocation: 0 (future-start goal, no allocation yet)
  • Monthly investment: 1,000,000
  • Annual return: 7%
  • Months from start: 1
  • Actual account growth: 0

Actual value:

currentValue = 0 + (0 × 100%)
= 0

Projected value:

projectedValue = 1,000,000

Determination:

0 >= 1,000,000 → OFF TRACK ✗

(Future goal with no allocation yet is off track)

Chart Rendering

The projected line on the chart is built by calculating projected value for each date interval.

Date Intervals

Based on time period selected:

  • Weeks: Every end-of-week from goal start to due date
  • Months: Every end-of-month from goal start to due date
  • Years: Every end-of-year from goal start to due date
  • All: Yearly intervals across entire goal timeline

Chart Data Points

For each date interval:

{
date: "2025-02-01",
dateLabel: "Feb '25",
projected: 1020321, // Calculated for end of period
actual: 34500000 // Historical account value (if available)
}

Handling Incomplete Periods

For the current period (which hasn't ended yet):

Current Logic (as of implementation):

  • If actual data available on or before the period end date: Use it
  • If NOT available AND it's the current period: Use latest known value
  • If future period: Show null (no actual data)

Example (December, current date is Dec 15):

  • Period end date: Dec 31 (future)
  • Latest actual value: Dec 15 (today)
  • Chart displays: Dec 15 value marked as "Dec" data point

Code:

// Lines 470-480 in use-goal-valuation-history.ts
if (actual === null && latestActualValue !== null) {
const isSamePeriod =
(period === "weeks" && format(date, "yyyy-ww") === format(today, "yyyy-ww")) ||
(period === "months" && format(date, "yyyy-MM") === format(today, "yyyy-MM")) ||
(period === "years" && format(date, "yyyy") === format(today, "yyyy")) ||
(period === "all" && format(date, "yyyy") === format(today, "yyyy"));

if (isSamePeriod) {
actual = latestActualValue; // Use today's value for current period
}
}

Projected vs Actual Lines

  • Projected (dashed line): Uses monthly compounding, calculated at period end dates
  • Actual (solid line): Historical values from account valuations, latest value used for current incomplete period

Special Cases

Case 1: Goal Not Started Yet

Setup:

  • Goal start date: 2026-01-01
  • Today: 2025-12-16
  • Months from start: negative

Handling:

if (monthsFromStart <= 0) return 0;

Projected value = 0 (goal hasn't started)


Case 2: No Monthly Investment

Setup:

  • Monthly investment: 0
  • Any return rate
  • Any months

Calculation:

projectedValue = 0

Projected value = 0 (only from contributions)


Case 3: Negative Return Rate

Not supported by business logic. Return rates are typically 0-20%. Negative returns should be handled as 0 or require separate loss calculation.

Data Dependencies

use-goal-progress.ts

Requires:

  • goal.monthlyInvestment — Monthly contribution amount
  • goal.targetReturnRate — Annual return percentage (0-100)
  • goal.startDate — Goal start date (ISO string)

Provides:

  • goalProgress.projectedValue — Projected value at today's date
  • goalProgress.isOnTrack — Boolean: currentValue >= projectedValue

use-goal-valuation-history.ts

Requires:

  • goal.monthlyInvestment — Monthly contribution amount
  • goal.targetReturnRate — Annual return percentage (0-100)
  • goal.startDate — Goal start date (ISO string)
  • goal.dueDate — Goal due date (ISO string)

Provides:

  • chartData[].projected — Projected value for each date interval
  • Used in chart rendering and visual comparison with actual values

Validation Rules

1. Monthly Investment Range

0 <= monthlyInvestment <= account_balance

Can be 0 (no contributions expected).

2. Annual Return Rate Range

0 <= annualReturnRate <= 100

Typically 0-20% for realistic scenarios.

3. Start Date Before Due Date

goal.startDate < goal.dueDate

Ensure goal timeline makes sense.

4. Projected Value Constraints

projectedValue >= 0

Always non-negative (can't have negative projected value without losses).

Testing Scenarios

  1. ✓ Goal with zero monthly investment → projected = 0
  2. ✓ Goal with zero return rate → projected = sum of contributions
  3. ✓ Goal with 0% monthly + 7% return → projected = 0
  4. ✓ Goal with contributions + return → compound interest applied
  5. ✓ Goal not started yet → projected = 0
  6. ✓ Goal active for 1 year → verify against manual calculation
  7. ✓ On-track determination → currentValue >= projectedValue
  8. ✓ Chart data points → verify intervals match selected period

References

  • src/lib/date-utils.ts — Date calculation utilities

    • getMonthsDiff() — Calculate months between dates
    • formatTimeRemaining() — Format time to due date
    • formatTimeElapsed() — Format time since start date
  • src/pages/goals/lib/goal-utils.ts — Goal calculation and status utilities

    • calculateProjectedValue() — Monthly compounding projection
    • calculateProjectedValueByDate() — Daily compounding projection for any date
    • getDaysDiff() — Calculate days between dates
    • isGoalOnTrack() — Compare current vs monthly-projected value
    • isGoalOnTrackByDate() — Compare current vs daily-projected value
    • isGoalScheduled() — Check if goal is future-scheduled
    • getGoalStatus() — Get UI status display
  • src/pages/goals/use-goal-progress.ts — Main hook for goal progress and on-track determination

  • src/pages/goals/use-goal-valuation-history.ts — Chart data generation with monthly compounding projections

  • src/pages/goals/goal-details-page.tsx — Goal details page (uses goalProgress.isOnTrack as single source)

  • src/pages/goals/components/goal-form.tsx — Goal form (calculates monthly investment using same formula)

  • docs/goal-actual-growth-business-logic.md — Actual value calculation logic