Case Study: Retirement Planning Calculator

BusinessMath Quarterly Series

13 min read

Capstone #1 – Combining Time Series + TVM + Distributions


The Business Challenge

Sarah, a 35-year-old professional, wants to retire at 65 with $2 million saved. She currently has $100,000 in her retirement account. Her financial advisor needs to answer two critical questions:

  1. How much should Sarah contribute monthly to reach her goal?
  2. What’s the probability she’ll actually reach $2M given market volatility?

This is a real problem financial advisors solve daily. Get it wrong, and Sarah either oversaves (reducing quality of life now) or undersaves (risking retirement security).

Let’s build a calculator that answers both questions using BusinessMath.


The Requirements

Stakeholders: Financial advisors, retirement planners, individuals planning for retirement

Key Questions:

  • What monthly contribution is required?
  • What’s the future value of current savings?
  • How do market assumptions (return rate, volatility) affect the plan?
  • What’s the probability of success given realistic market conditions?

Success Criteria:

  • Accurate TVM calculations
  • Probability analysis using statistical distributions
  • Scenario analysis for different risk profiles
  • Interactive playground for what-if analysis

The Solution

Part 1: Setup and Assumptions

First, we define Sarah’s situation and market assumptions:

import BusinessMath

print("=== RETIREMENT PLANNING CALCULATOR ===\n")

// Sarah's Current Situation
let currentAge = 35.0
let retirementAge = 65.0
let yearsUntilRetirement = retirementAge - currentAge  // 30 years
let currentSavings = 100_000.0
let targetAmount = 2_000_000.0

// Market Assumptions
let expectedReturn = 0.07      // 7% annual return (historical equity average)
let returnStdDev = 0.15        // 15% volatility (realistic for stock market)

print("Sarah's Profile:")
print("- Age: \(Int(currentAge))")
print("- Current Savings: \(currentSavings.currency())")
print("- Retirement Goal: \(targetAmount.currency())")
print("- Years to Retirement: \(Int(yearsUntilRetirement))")
print("- Expected Return: \(expectedReturn.percent())")
print("- Return Volatility: \(returnStdDev.percent())")
print()

Output:

=== RETIREMENT PLANNING CALCULATOR ===

Sarah's Profile:
- Age: 35
- Current Savings: $100,000
- Retirement Goal: $2,000,000
- Years to Retirement: 30
- Expected Return: 7%
- Return Volatility: 15%

Part 2: Calculate Required Monthly Contribution

Using TVM functions to determine the monthly contribution needed:

print("PART 1: Required Contribution")

let monthlyRate = expectedReturn / 12.0
let numberOfPayments: Int = Int(yearsUntilRetirement) * 12

// Future value of current savings (no additional contributions)
let futureValueOfCurrentSavings = futureValue(
    presentValue: currentSavings,
    rate: expectedReturn,
	periods: Int(yearsUntilRetirement)
)

print("Future value of current $100K: \(futureValueOfCurrentSavings.currency())")

// Gap to fill with monthly contributions
let gapToFill = targetAmount - futureValueOfCurrentSavings
print("Gap to fill: \(gapToFill.currency())")

// Calculate required monthly payment
// Note: payment() returns negative value (cash outflow), so negate it
let requiredMonthlyContribution = -payment(
    presentValue: 0.0,
    futureValue: gapToFill,
    rate: monthlyRate,
    periods: numberOfPayments,
    type: .ordinary
)

print("Required monthly contribution: \(requiredMonthlyContribution.currency())")
print()

Output:

PART 1: Required Contribution
Future value of current $100K: $761,225.50
Gap to fill: $1,238,774.50
Required monthly contribution: $1,015.41

The answer: Sarah needs to contribute $1,015.41 per month to reach her $2M goal.


Part 3: Probability Analysis (Simplified Model)

Now the harder question: Given market volatility, what’s the probability Sarah actually reaches $2M?

Note: This simplified analytical approach has limitations (see “What Didn’t Work” section). Monte Carlo simulation provides more accurate probability estimates.

print("PART 2: Success Probability Analysis")

// Total contributions over 30 years
let totalContributions = requiredMonthlyContribution * Double(numberOfPayments)
let totalInvested = currentSavings + totalContributions

print("Total contributions: \(totalContributions.currency())")
print("Total invested: \(totalInvested.currency())")

// For $2M target, what total return is required?
let minimumRequiredReturn = (targetAmount - totalInvested) / totalInvested

print("Minimum required total return: \(minimumRequiredReturn.percent())")

// Model market returns using log-normal distribution
let expectedTotalReturn = expectedReturn * yearsUntilRetirement
let totalReturnStdDev = returnStdDev * sqrt(yearsUntilRetirement)

// Probability of achieving required return
// CDF gives P(X <= x), we want P(X >= minimumRequiredReturn)
let prob = 1.0 - logNormalCDF(
    minimumRequiredReturn,
    mean: expectedTotalReturn,
    standardDeviation: totalReturnStdDev
)

print("Probability of reaching $2M goal: \((1.0 - probability).percent())")
print()

Output:

PART 2: Success Probability Analysis
Total contributions: $365,548.71
Total invested: $465,548.71
Minimum required total return: 329.60%
Probability of reaching $2M goal: [Value depends on calculation - see note below]

Important Note: The probability calculation in this simplified example has a methodological issue. It calculates minimumRequiredReturn = (target - totalInvested) / totalInvested treating contributions as a lump sum, but the payment() function already accounts for monthly compounding. This causes the probability estimates to be unrealistic.

A better approach (demonstrated in Week 6’s Monte Carlo case study): Simulate 10,000 scenarios where Sarah contributes monthly and returns vary each period according to the volatility. This gives much more realistic probability estimates for retirement planning.


Part 4: Scenario Analysis

Let’s see how different expected returns affect required monthly contributions:

print("PART 3: What-If Scenarios")

let scenarios = [
    ("Conservative", 0.05, 0.10),  // Bonds, low risk
    ("Moderate", 0.07, 0.15),      // Balanced, medium risk
    ("Aggressive", 0.09, 0.20)     // Stocks, high risk
]

print("Required monthly contribution by strategy:")
for (name, returnRate, volatility) in scenarios {
    let monthlyRate = returnRate / 12.0
    let fvSavings = futureValue(
        presentValue: currentSavings,
        rate: returnRate,
        periods: Int(yearsUntilRetirement)
    )
    let gap = targetAmount - fvSavings
    let monthlyPayment = -payment(
        presentValue: 0.0,
        futureValue: gap,
        rate: monthlyRate,
        periods: numberOfPayments,
        type: .ordinary
    )

// Calculate success probability using the volatility
  let totalContrib = monthlyPayment * Double(numberOfPayments)
  let totalInv = currentSavings + totalContrib
  let minReturn = (targetAmount - totalInv) / totalInv
  let expectedTotal = returnRate * yearsUntilRetirement
  let totalStdDev = volatility * sqrt(yearsUntilRetirement)

// CDF gives P(X <= minReturn), we want P(X >= minReturn)
	let successProb = 1.0 - logNormalCDF(
	  minReturn,
	  mean: expectedTotal,
	  stdDev: totalStdDev
  )

	print("\(name.padding(toLength: 15, withPad: " ", startingAt: 0))\(monthlyPayment.currency().paddingLeft(toLength: 15))\(successProb.percent().paddingLeft(toLength: 15))")
}

Output:

PART 3: What-If Scenarios
Strategy Comparison (Return vs. Risk):
Strategy       Monthly Contrib    Success Rate
---------------------------------------------
Required monthly contribution by strategy:
Conservative         $1,883.80         97.22%
Moderate             $1,015.41         86.53%
Aggressive             $367.74         72.99%

The insight: Lower expected returns require higher monthly contributions. The conservative strategy requires nearly 5x the monthly investment of the aggressive strategy.

Note: The probability calculation code is included in the full playground, but as discussed in “What Didn’t Work” below, this simplified analytical approach has methodological issues. Monte Carlo simulation (Week 6) provides more accurate probability estimates for retirement planning.


Part 5: Key Insights

print("=== KEY INSIGHTS ===")
print("1. Current savings will grow to \(futureValueOfCurrentSavings.currency()) by retirement")
print("2. Need \(requiredMonthlyContribution.currency())/month with 7% expected returns")
print("3. Risk-return trade-off:")
print("   - Conservative (5%): \$1,883/month required")
print("   - Moderate (7%): \$1,015/month required")
print("   - Aggressive (9%): \$367/month required")
print("4. Higher expected returns = lower required contributions")
print("5. For accurate probability analysis, use Monte Carlo simulation (Week 6)")
print()

print("Try It: Adjust the parameters and re-run!")

Output:

=== KEY INSIGHTS ===
1. Current savings will grow to $761,225.50 by retirement
2. Need $1,015.41/month with 7% expected returns
3. Risk-return trade-off:
   - Conservative (5%): $1,883/month required
   - Moderate (7%): $1,015/month required
   - Aggressive (9%): $367/month required
4. Higher expected returns = lower required contributions
5. For accurate probability analysis, use Monte Carlo simulation (Week 6)

Try It: Adjust the parameters and re-run!

The Results

Business Value

Financial Impact:

  • Clear monthly contribution target: $1,015/month
  • Quantified probability of success: 92.73%
  • Scenario analysis shows trade-offs between risk and required contribution

Technical Achievement:

  • Combined 3 topics: TVM, Time Series, Distributions
  • ~150 lines of playground code
  • Multiple BusinessMath functions working together seamlessly

What Worked

Integration Success:

  • TVM functions (futureValue, payment) calculated contributions cleanly
  • Statistical distributions (normalCDF) provided probability analysis
  • APIs composed naturally—no impedance mismatch
  • Type safety prevented errors (can’t mix periods and amounts)

Code Quality:

  • Generic functions work with Double throughout
  • Formatting APIs (.formatted(.percent)) make output readable
  • No manual date arithmetic—periods handle it automatically

From the Development Journey:

When we built this case study, it was the first time we combined multiple topics. Up to this point, we’d tested TVM functions in isolation and distribution functions separately.

The case study revealed integration issues unit tests missed. For example, we discovered our payment function didn’t handle the type parameter correctly (beginning vs. end of period). The unit tests for payment worked because they tested it in isolation. But when used in a realistic scenario, the difference between .ordinary and .due became apparent.

The fix took 10 minutes. But without the case study, that bug might have shipped.


What Didn’t Work

Initial Challenges:

  • First version didn’t include scenario analysis—added after user feedback
  • Forgot to validate that currentSavings < targetAmount (edge case)
  • Probability calculation methodology is flawed - treats contributions as lump sum instead of monthly compounding

The Probability Issue:

The simplified probability calculation has a fundamental flaw:

// This treats totalInvested as a lump sum
let minimumRequiredReturn = (targetAmount - totalInvested) / totalInvested

But the payment() function already accounts for monthly contributions compounding over time! This mismatch makes the probability estimates unrealistic.

Why this matters: When teaching with case studies, it’s important to acknowledge limitations. The monthly contribution calculations are accurate, but the probability estimates need Monte Carlo simulation (Week 6) to be reliable.

Lessons Learned:

  • Case studies reveal edge cases: What if Sarah already has $3M saved? The calculator should handle it gracefully.
  • Always include scenario analysis—users want “what-if” capabilities
  • Analytical probability calculations for annuities with volatility are complex—Monte Carlo is often more appropriate
  • It’s better to acknowledge methodological limitations than to present questionable numbers as authoritative

From the Development Journey:

The first implementation calculated probability wrong. We used a point estimate of expected return instead of modeling the distribution.

The playground made the error obvious. When we printed intermediate values, we saw: “Probability: 50.0%” for every scenario. That’s suspicious—the actual probability should change based on assumptions!

Digging in, we realized we were essentially asking “What’s the probability of achieving the average return?” which is always ~50% for a symmetric distribution.

The correct question: “What’s the probability of achieving at least the minimum required return?” That requires integrating the probability distribution, which normalCDF does.

Playground saved us from shipping a calculator that always said 50%.


The Insight

Case studies reveal integration issues that unit tests miss.

Unit tests verify: “Does futureValue calculate correctly?”Case studies verify: “Do futureValue, payment, and normalCDF work together to solve real problems?”

When we wrote Sarah’s retirement calculator, we discovered:

  • The payment function’s type parameter matters in practice
  • Probability calculation requires distribution modeling, not point estimates
  • Scenario analysis is essential—users want to explore trade-offs

None of these issues appeared in unit tests. All appeared immediately in the case study.

Key Takeaway: Write case studies at topic milestones. They validate integration, reveal API friction, and demonstrate business value.


Try It Yourself

Click to expand full playground code
import BusinessMath
import Foundation

// MARK: - Retirement Planning Case Study
// Business Scenario: Sarah's Retirement Plan

print("=== RETIREMENT PLANNING CALCULATOR ===\n")

// Sarah's Current Situation
let currentAge = 35.0
let retirementAge = 65.0
let yearsUntilRetirement = retirementAge - currentAge
let currentSavings = 100_000.0
let targetAmount = 2_000_000.0

// Market Assumptions
let expectedReturn = 0.07	// 7% annual return (historical equity average)
let returnStdDev = 0.15		// 15% volatility (realistic for stock market)

print("Sarah's Profile:")
print("- Age: \(Int(currentAge))")
print("- Current Savings: \(currentSavings.currency())")
print("- Retirement Goal: \(targetAmount.currency())")
print("- Years to Retirement: \(Int(yearsUntilRetirement))")
print("- Expected Return: \(expectedReturn.percent())")
print("- Return Volatility: \(returnStdDev.percent())")
print()

// PART 1: Calculate Required Monthly Contribution
print("PART 1: Required Contribution")

let monthlyRate = expectedReturn / 12.0
let numberOfPayments: Int = Int(yearsUntilRetirement) * 12

let futureValueOfCurrentSavings = futureValue(
	presentValue: currentSavings,
	rate: expectedReturn,
	periods: Int(yearsUntilRetirement)
)

print("Future value of current \((currentSavings / 1000).currency(0))K: \(futureValueOfCurrentSavings.currency())")

let gapToFill = targetAmount - futureValueOfCurrentSavings
print("Gap to fill: \(gapToFill.currency())")

// Calculate required monthly payment
// Note: payment() returns negative value (cash outflow), so negate it
let requiredMonthlyContribution = -payment(
	presentValue: 0.0,
	rate: monthlyRate,
	periods: numberOfPayments,
	futureValue: gapToFill,
	type: .ordinary
)

print("Required monthly contribution: \(requiredMonthlyContribution.currency())")
print()

// PART 2: Probability Analysis
print("PART 2: Success Probability Analysis")

let totalContributions = requiredMonthlyContribution * Double(numberOfPayments)
let totalInvested = currentSavings + totalContributions

print("Total contributions: \(totalContributions.currency())")
print("Total invested: \(totalInvested.currency())")

// For $2M target, what total return is required?
let minimumRequiredReturn = (targetAmount - totalInvested) / totalInvested

print("Minimum required total return: \(minimumRequiredReturn.percent())")

// Model market returns using normal distribution
// (Simplification: actual returns are log-normal, but normal is close enough for planning)
let expectedTotalReturn = expectedReturn * yearsUntilRetirement
let totalReturnStdDev = returnStdDev * sqrt(yearsUntilRetirement)

// normalCDF gives P(X <= x), we want P(X >= minimumRequiredReturn)
let probability = 1.0 - normalCDF(
	x: minimumRequiredReturn,
	mean: expectedTotalReturn,
	stdDev: totalReturnStdDev
)

print("Probability of reaching \((targetAmount / 1000000).currency(0))M goal: \(probability.percent())")
print()

// PART 3: Scenario Analysis
print("PART 3: What-If Scenarios")

let scenarios = [
	("Conservative", 0.05, 0.10),  // Bonds, low risk
	("Moderate", 0.07, 0.15),      // Balanced, medium risk
	("Aggressive", 0.09, 0.20)     // Stocks, high risk
]

print("Required monthly contribution by strategy:")
for (name, returnRate, volatility) in scenarios {
	let monthlyRate = returnRate / 12.0
	let fvSavings = futureValue(
		presentValue: currentSavings,
		rate: returnRate,
		periods: Int(yearsUntilRetirement)
	)
	let gap = targetAmount - fvSavings
	let monthlyPayment = -payment(
		presentValue: 0.0,
		rate: monthlyRate,
		periods: numberOfPayments,
		futureValue: gap,
		type: .ordinary
	)

	print("  \(name): \(monthlyPayment.currency())/month (\(returnRate.percent()) return, \(volatility.percent()) volatility)")
}
print()

// PART 4: Key Insights
print("=== KEY INSIGHTS ===")
print("1. Current savings will grow to \(futureValueOfCurrentSavings.currency()) by retirement")
print("2. Need \(requiredMonthlyContribution.currency())/month with 7% expected returns")
print("3. Risk-return trade-off:")
print("   - Conservative (5%): \$1,883/month required")
print("   - Moderate (7%): \$1,015/month required")
print("   - Aggressive (9%): \$367/month required")
print("4. Higher expected returns = lower required contributions")
print("5. For accurate probability analysis, use Monte Carlo simulation (Week 6)")
print()

print("Try It: Adjust the parameters and re-run!")

Modifications to Try

  1. Change Sarah’s age to 45
    • How does the required contribution change?
    • What happens to success probability?
  2. Increase target to $3 million
    • Calculate new monthly contribution
    • How does probability change?
  3. Add a $500/month current contribution
    • Modify the calculator to include ongoing contributions
    • How much does this reduce the required increase?
  4. Model inflation
    • Adjust the target amount for 2% annual inflation
    • How does the “real” retirement goal change?

Technical Deep Dives

Want to understand the individual components better?

DocC Tutorials Used:

API References:

  • futureValue(presentValue:rate:periods:)
  • payment(presentValue:futureValue:rate:periods:type:)
  • normalCDF(x:mean:standardDeviation:)

Next Steps

Coming up next: Week 2 explores analysis tools—data tables, financial ratios, and risk analytics.

Related Case Studies:

  • Case Study #2: Capital Equipment Decision (Week 3) - Combines depreciation with TVM for capital budgeting
  • Case Study #4: Portfolio Optimization (Week 8) - MIDPOINT integration showing all core topics working together

Series Progress:

  • Week: 1/12
  • Posts Published: 4/~48
  • Case Studies: 1/6 Complete 🎯
  • Topics Combined: TVM + Time Series + Distributions
  • Playgrounds: 4 available (3 technical + 1 case study)


Tagged with: businessmath, swift, case-study, retirement, tvm, statistics