BusinessMath Quarterly Series
13 min read
Capstone #1 – Combining Time Series + TVM + Distributions
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:
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.
Stakeholders: Financial advisors, retirement planners, individuals planning for retirement
Key Questions:
Success Criteria:
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%
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.
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.
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.
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!
Financial Impact:
Technical Achievement:
Integration Success:
futureValue, payment) calculated contributions cleanlynormalCDF) provided probability analysisCode Quality:
.formatted(.percent)) make output readableFrom 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
paymentfunction didn’t handle thetypeparameter correctly (beginning vs. end of period). The unit tests forpaymentworked because they tested it in isolation. But when used in a realistic scenario, the difference between.ordinaryand.duebecame apparent.The fix took 10 minutes. But without the case study, that bug might have shipped.
Initial Challenges:
currentSavings < targetAmount (edge case)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:
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
normalCDFdoes.Playground saved us from shipping a calculator that always said 50%.
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:
payment function’s type parameter matters in practiceNone 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.
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!")
Want to understand the individual components better?
DocC Tutorials Used:
futureValue, payment)normalCDF for probabilityAPI References:
futureValue(presentValue:rate:periods:)payment(presentValue:futureValue:rate:periods:type:)normalCDF(x:mean:standardDeviation:)Coming up next: Week 2 explores analysis tools—data tables, financial ratios, and risk analytics.
Related Case Studies:
Series Progress:
Tagged with: businessmath, swift, case-study, retirement, tvm, statistics