Monte Carlo Simulation for Financial Forecasting
BusinessMath Quarterly Series
23 min read
Part 20 of 12-Week BusinessMath Series
What You’ll Learn
- Building probabilistic forecasts with uncertainty quantification
- Projecting revenue with compounding growth and randomness
- Creating complete income statement forecasts with multiple uncertain drivers
- Calculating confidence intervals (90%, 95%) for projections
- Extracting mean, median, and percentile scenarios
- Optimizing Monte Carlo simulations for performance
The Problem
Traditional financial forecasts give you a single number: “Revenue next quarter: $1M.” But reality is uncertain:- What if growth varies? Expected 10% growth might be anywhere from 5%-15%.
- How likely is profitability? Is there a 50% chance or 95% chance we’re profitable?
- What’s the downside risk? In the worst 5% of scenarios, how bad does it get?
- How do uncertainties combine? When both revenue AND costs are uncertain, what’s the total impact?
The Solution
Monte Carlo simulation runs thousands of scenarios, each with different random values from probability distributions (like a roulette wheel, hence the name). Instead of “Revenue = $1M”, you get “Revenue: Mean $1M, but with a 90% Confidence Interval ranging from $850K to $1.15M”.BusinessMath provides probabilistic drivers, simulation infrastructure, and statistical analysis to enable you to build more robust forecasts based off a range of values.
Single Metric with Growth Uncertainty
Start simple: project revenue with uncertain quarterly growth:import BusinessMath
// Historical revenue
let baseRevenue = 1_000_000.0 // $1M
// Growth rate uncertainty: mean 10%, std dev 5%
let growthDriver = ProbabilisticDriver
.normal(
name: “Quarterly Growth”,
mean: 0.10, // Expected 10% per quarter
stdDev: 0.05 // ±5% uncertainty
)
// Project 4 quarters
let q1 = Period.quarter(year: 2025, quarter: 1)
let quarters = [q1, q1 + 1, q1 + 2, q1 + 3]
// Run Monte Carlo simulation (10,000 paths)
let iterations = 10_000
// Pre-allocate for performance
var allValues: [[Double]] = Array(repeating: [], count: quarters.count)
for i in 0…(quarters.count - 1) {
allValues[i].reserveCapacity(iterations)
}
// Generate revenue paths with compounding
for _ in 0…(iterations - 1) {
var currentRevenue = baseRevenue
for (periodIndex, period) in quarters.enumerated() {
let growth = growthDriver.sample(for: period)
currentRevenue = currentRevenue * (1.0 + growth) // Compound!
allValues[periodIndex].append(currentRevenue)
}
}
// Calculate statistics for each period
var statistics: [Period: SimulationStatistics] = [:]
var percentiles: [Period: Percentiles] = [:]
for (periodIndex, period) in quarters.enumerated() {
let results = SimulationResults(values: allValues[periodIndex])
statistics[period] = results.statistics
percentiles[period] = results.percentiles
}
// Display results
print(“Revenue Forecast with Compounding Growth”)
print(”=========================================”)
print(“Base Revenue: (baseRevenue.currency(0))”)
print(“Quarterly Growth: 10% ± 5% (compounding)”)
print()
print(“Quarter Mean Median 90% CI Growth”)
print(”—–– ––––– ––––– –––––––––––––– —––”)
for quarter in quarters {
let stats = statistics[quarter]!
let pctiles = percentiles[quarter]!
let growth = (stats.mean - baseRevenue) / baseRevenue
print(”(quarter.label) (stats.mean.currency(0)) (pctiles.p50.currency(0)) [(pctiles.p5.currency(0)), (pctiles.p95.currency(0))] (growth.percent(1))”)
}
Output:
Revenue Forecast with Compounding Growth
=========================================
Base Revenue: $1,000,000
Quarterly Growth: 10% ± 5% (compounding)
Quarter Mean Median 90% CI Growth
—–– ––––– ––––– ———————— —––
2025-Q1 $1,098,850 $1,098,794 [$1,017,019, $1,180,785] 9.9%
2025-Q2 $1,208,699 $1,207,346 [$1,084,379, $1,337,429] 20.9%
2025-Q3 $1,328,825 $1,325,827 [$1,162,620, $1,506,463] 32.9%
2025-Q4 $1,462,127 $1,454,999 [$1,250,988, $1,692,565] 46.2%
The insights:
- Compounding accelerates: 46.4% total growth (not 40% = 4 × 10%)
- Uncertainty widens: Q1 CI width = $165K, Q4 = $442K (2.7× wider)
- Assymetric distribution: Mean slightly > Median (right-skewed from compounding)
###Critical Implementation Detail: Compounding
The key to proper compounding is generating complete paths in each iteration:
// ✓ CORRECT: Complete path per iteration
for iteration in 1…10_000 {
var revenue = baseRevenue
for period in periods {
revenue *= (1 + sampleGrowth()) // Compounds across periods
recordValue(period, revenue)
}
}
// ✗ WRONG: Each period sampled independently
for period in periods {
for iteration in 1…10_000 {
let revenue = baseRevenue * (1 + sampleGrowth()) // No compounding!
recordValue(period, revenue)
}
}
Why this matters: In the correct approach, Q2 revenue is based on Q1’s realized revenue, not the original base. This creates path-dependency and realistic compounding.
Extract Scenario Time Series
Convert simulation results to concrete scenarios:// Build time series at different confidence levels
let expectedValues = quarters.map { statistics[$0]!.mean }
let medianValues = quarters.map { percentiles[$0]!.p50 }
let p5Values = quarters.map { percentiles[$0]!.p5 }
let p95Values = quarters.map { percentiles[$0]!.p95 }
let expectedRevenue = TimeSeries(periods: quarters, values: expectedValues)
let medianRevenue = TimeSeries(periods: quarters, values: medianValues)
let conservativeRevenue = TimeSeries(periods: quarters, values: p5Values)
let optimisticRevenue = TimeSeries(periods: quarters, values: p95Values)
print(”\nScenario Projections:”)
print(“Conservative (P5): (conservativeRevenue.valuesArray.map { $0.currency(0) })”)
print(“Median (P50): (medianRevenue.valuesArray.map { $0.currency(0) })”)
print(“Expected (mean): (expectedRevenue.valuesArray.map { $0.currency(0) })”)
print(“Optimistic (P95): (optimisticRevenue.valuesArray.map { $0.currency(0) })”)
Output:
Scenario Projections:
Conservative (P5): [”$1,018,650”, “$1,085,428”, “$1,163,683”, “$1,252,460”]
Median (P50): [”$1,099,356”, “$1,208,034”, “$1,327,870”, “$1,457,335”]
Expected (mean): [”$1,099,955”, “$1,209,512”, “$1,330,944”, “$1,463,487”]
Optimistic (P95): [”$1,181,847”, “$1,339,856”, “$1,510,784”, “$1,693,091”]
Usage: These time series can feed into budget planning, NPV calculations, or dashboard visualization.
Complete Income Statement Forecast
Now build a full P&L with multiple uncertain drivers:// Define probabilistic drivers
struct IncomeStatementDrivers {
let unitsSold: ProbabilisticDriver
let averagePrice: ProbabilisticDriver
let cogs: ProbabilisticDriver
// % of revenue
let opex: ProbabilisticDriver
init() {
// Units: Normal distribution (mean 10K, std 1K)
self.unitsSold = .normal(
name: “Units Sold”,
mean: 10_000.0,
stdDev: 1_000.0
)
// Price: Triangular distribution (most likely $100, range $95-$110)
self.averagePrice = .triangular(
name: “Average Price”,
low: 95.0,
high: 110.0,
base: 100.0
)
// COGS as % of revenue: Normal (mean 60%, std 3%)
self.cogs = .normal(
name: “COGS %”,
mean: 0.60,
stdDev: 0.03
)
// OpEx: Normal (mean $200K, std $20K)
self.opex = .normal(
name: “Operating Expenses”,
mean: 200_000.0,
stdDev: 20_000.0
)
}
}
let drivers = IncomeStatementDrivers()
let periods = Period.year(2025).quarters()
// Run simulation manually for full control
var revenueValues: [[Double]] = Array(repeating: [], count: periods.count)
var grossProfitValues: [[Double]] = Array(repeating: [], count: periods.count)
var opIncomeValues: [[Double]] = Array(repeating: [], count: periods.count)
for i in 0…(periods.count - 1) {
revenueValues[i].reserveCapacity(iterations)
grossProfitValues[i].reserveCapacity(iterations)
opIncomeValues[i].reserveCapacity(iterations)
}
for _ in 0…(iterations - 1) {
for (periodIndex, period) in periods.enumerated() {
// Sample all drivers
let units = drivers.unitsSold.sample(for: period)
let price = drivers.averagePrice.sample(for: period)
let cogsPercent = drivers.cogs.sample(for: period)
let opexAmount = drivers.opex.sample(for: period)
// Calculate P&L
let revenue = units * price
let grossProfit = revenue * (1.0 - cogsPercent)
let operatingIncome = grossProfit - opexAmount
// Record
revenueValues[periodIndex].append(revenue)
grossProfitValues[periodIndex].append(grossProfit)
opIncomeValues[periodIndex].append(operatingIncome)
}
}
// Calculate statistics
var revenueStats: [Period: SimulationStatistics] = [:]
var revenuePctiles: [Period: Percentiles] = [:]
var gpStats: [Period: SimulationStatistics] = [:]
var gpPctiles: [Period: Percentiles] = [:]
var opStats: [Period: SimulationStatistics] = [:]
var opPctiles: [Period: Percentiles] = [:]
for (periodIndex, period) in periods.enumerated() {
let revResults = SimulationResults(values: revenueValues[periodIndex])
revenueStats[period] = revResults.statistics
revenuePctiles[period] = revResults.percentiles
let gpResults = SimulationResults(values: grossProfitValues[periodIndex])
gpStats[period] = gpResults.statistics
gpPctiles[period] = gpResults.percentiles
let opResults = SimulationResults(values: opIncomeValues[periodIndex])
opStats[period] = opResults.statistics
opPctiles[period] = opResults.percentiles
}
// Display comprehensive forecast
print(”\nIncome Statement Forecast - 2025”)
print(”==================================”)
for quarter in periods {
print(”\n(quarter.label)”)
print(String(repeating: “-”, count: 60))
// Revenue
let revS = revenueStats[quarter]!
let revP = revenuePctiles[quarter]!
print(“Revenue”)
print(” Expected: (revS.mean.currency(0))”)
print(” Std Dev: (revS.stdDev.currency(0)) (CoV: ((revS.stdDev / revS.mean).percent(1)))”)
print(” 90% CI: [(revP.p5.currency(0)), (revP.p95.currency(0))]”)
// Gross Profit
let gpS = gpStats[quarter]!
let gpP = gpPctiles[quarter]!
let gpMargin = gpS.mean / revS.mean
print(”\nGross Profit”)
print(” Expected: (gpS.mean.currency(0)) ((gpMargin.percent(1)) margin)”)
print(” 90% CI: [(gpP.p5.currency(0)), (gpP.p95.currency(0))]”)
// Operating Income
let opS = opStats[quarter]!
let opP = opPctiles[quarter]!
let opMargin = opS.mean / revS.mean
print(”\nOperating Income”)
print(” Expected: (opS.mean.currency(0)) ((opMargin.percent(1)) margin)”)
print(” 90% CI: [(opP.p5.currency(0)), (opP.p95.currency(0))]”)
// Risk assessment
let profitProb = opP.p5 > 0 ? 100 : (opP.p25 > 0 ? 75 : (opP.p50 > 0 ? 50 : 25))
print(”\nRisk: Probability of profit ~(profitProb)%”)
}
Output (Q1 sample):
Income Statement Forecast - 2025
==================================
2025-Q1
————————————————————
Revenue
Expected: $1,016,299
Std Dev: $107,047 (CoV: 10.5%)
90% CI: [$847,015, $1,196,331]
Gross Profit
Expected: $406,751 (40.0% margin)
90% CI: [$323,310, $497,667]
Operating Income
Expected: $206,721 (20.3% margin)
90% CI: [$116,827, $302,834]
Risk: Probability of profit ~100%
2025-Q2
————————————————————
Revenue
Expected: $1,015,920
Std Dev: $105,407 (CoV: 10.4%)
90% CI: [$844,988, $1,190,621]
Gross Profit
Expected: $406,658 (40.0% margin)
90% CI: [$322,571, $496,529]
Operating Income
Expected: $206,519 (20.3% margin)
90% CI: [$118,335, $300,982]
Risk: Probability of profit ~100%
2025-Q3
————————————————————
Revenue
Expected: $1,015,300
Std Dev: $106,692 (CoV: 10.5%)
90% CI: [$842,995, $1,192,164]
Gross Profit
Expected: $406,423 (40.0% margin)
90% CI: [$323,562, $496,073]
Operating Income
Expected: $206,643 (20.4% margin)
90% CI: [$116,430, $301,980]
Risk: Probability of profit ~100%
2025-Q4
————————————————————
Revenue
Expected: $1,016,188
Std Dev: $106,038 (CoV: 10.4%)
90% CI: [$841,278, $1,188,276]
Gross Profit
Expected: $406,515 (40.0% margin)
90% CI: [$323,719, $495,371]
Operating Income
Expected: $206,563 (20.3% margin)
90% CI: [$117,112, $302,171]
Risk: Probability of profit ~100%
The power: You now have a complete probabilistic P&L showing expected values, confidence intervals, and risk metrics for every line item. But note, this doesn’t just have to be done for financial models. Anything that you want to model with uncertainty can be simulated this way
Performance Optimization
For basic monte carlo simulation runs, optimizations may not be worth the lift, but for large simulations (50K+ iterations), we have some recommendations to really maximize performance:// 1. Pre-allocate arrays
var values: [Double] = []
values.reserveCapacity(iterations) // Avoids repeated reallocation
// 2. Store by period, not by path
// Good: allValues[periodIndex][iterationIndex]
// Bad: allPaths[iterationIndex][periodIndex] (poor cache locality)
// 3. Inline calculations instead of function calls
// The function call overhead matters at 10M+ samples
// 4. Use SimulationResults for statistics
// It sorts once and calculates all percentiles efficiently
let results = SimulationResults(values: values)
let p5 = results.percentiles.p5 // ✓ Fast (already sorted)
let mean = results.statistics.mean // ✓ Fast (already computed)
Performance benchmark: 10,000 iterations × 20 periods = 200K samples runs in < 1 second on modern hardware with these optimizations.
GPU-Accelerated Expression Models for Single-Period Calculations
For single-period calculations with high iteration counts, BusinessMath providesMonteCarloExpressionModel - a GPU-accelerated approach that delivers 10-100× speedup with minimal memory usage. We’ve got
a deeper dive on GPU acceleration here.
When to use expression models:
- ✅ Single-period calculations (no compounding across time)
- ✅ High iteration counts (50,000+)
- ✅ Compute-intensive formulas
- ✅ Memory-constrained environments
- ✅ Multi-period compounding (like the revenue growth example above)
- ✅ Complex state management across periods
- ✅ Path-dependent calculations
import BusinessMath
// Pre-compute any constants
let taxRate = 0.21
// Define the P&L model using expression builder
let incomeStatementModel = MonteCarloExpressionModel { builder in
// Inputs: units, price, cogsPercent, opex
let units = builder[0]
let price = builder[1]
let cogsPercent = builder[2]
let opex = builder[3]
// Calculate revenue and costs
let revenue = units * price
let cogs = revenue * cogsPercent
let grossProfit = revenue - cogs
let ebitda = grossProfit - opex
// Conditional tax (only pay tax if profitable)
let isProfitable = ebitda.greaterThan(0.0)
let tax = isProfitable.ifElse(
then: ebitda * taxRate,
else: 0.0
)
let netIncome = ebitda - tax
return netIncome // Return what we’re simulating
}
// Set up high-performance simulation
var simulation = MonteCarloSimulation(
iterations: 100_000, // 10× more iterations than before
enableGPU: true,
expressionModel: incomeStatementModel
)
// Add input distributions (order matches builder[0], builder[1], etc.)
simulation.addInput(SimulationInput(
name: “Units Sold”,
distribution: DistributionNormal(mean: 10_000, stdDev: 1_000)
))
simulation.addInput(SimulationInput(
name: “Average Price”,
distribution: DistributionTriangular(low: 95, high: 110, mode: 100)
))
simulation.addInput(SimulationInput(
name: “COGS Percentage”,
distribution: DistributionNormal(mean: 0.60, stdDev: 0.03)
))
simulation.addInput(SimulationInput(
name: “Operating Expenses”,
distribution: DistributionNormal(mean: 200_000, stdDev: 20_000)
))
// Run simulation
let results = try simulation.run()
// Display results
print(“GPU-Accelerated Income Statement Forecast”)
print(”==========================================”)
print(“Iterations: (results.iterations.formatted())”)
print(“Compute Time: (results.computeTime.formatted(.number.precision(.fractionLength(1)))) ms”)
print(“GPU Used: (results.usedGPU ? “Yes” : “No”)”)
print()
print(“Net Income After Tax:”)
print(” Mean: (results.statistics.mean.currency(0))”)
print(” Median: (results.percentiles.p50.currency(0))”)
print(” Std Dev: (results.statistics.stdDev.currency(0))”)
print(” 95% CI: [(results.percentiles.p5.currency(0)), (results.percentiles.p95.currency(0))]”)
print()
// Risk metrics
let profitableCount = results.valuesArray.filter { $0 > 0 }.count
let profitabilityRate = Double(profitableCount) / Double(results.iterations)
print(“Risk Metrics:”)
print(” Probability of Profit: (profitabilityRate.percent(1))”)
print(” Value at Risk (5%): (results.percentiles.p5.currency(0))”)
Output:
GPU-Accelerated Income Statement Forecast
==========================================
Iterations: 100,000
Compute Time: 45.2 ms
GPU Used: Yes
Net Income After Tax:
Mean: $163,287
Median: $163,402
Std Dev: $66,148
95% CI: [$54,076, $278,331]
Risk Metrics:
Probability of Profit: 99.2%
Value at Risk (5%): $54,076
Performance comparison:
| Approach | Iterations | Time | Memory | Speedup |
|---|---|---|---|---|
| Traditional loops | 10,000 | ~850 ms | ~15 MB | 1× (baseline) |
| GPU Expression Model | 100,000 | ~45 ms | ~8 MB | ~189× |
★ Insight ─────────────────────────────────────
Understanding Constants vs Variables in Expression Models
Expression models use a special DSL (domain-specific language) that compiles to GPU bytecode. This creates two distinct “worlds”:
Swift World (outside the builder):
let taxRate = 0.21 // Regular Swift Double
let multiplier = pow(1.05, 10) // Use Swift’s pow() for constants
DSL World (inside the builder):
let revenue = units * price // ExpressionProxy objects
let afterTax = revenue * (1.0 - taxRate) // Use pre-computed constant
Critical Rule: Pre-compute all constants outside the builder using Swift Foundation’s standard functions (
pow(),
sqrt(),
exp(), etc.). Inside the builder, only use DSL methods (
.exp(),
.sqrt(),
.power()) on variables that depend on random inputs.
Why? GPU methods have to be pre-compiled for the GPU to do it’s magic and optimize calculation. The builder creates an expression tree that gets compiled to bytecode and sent to the GPU. Constants should be baked into the bytecode, not recomputed millions of times.
// ❌ WRONG: Computing constants inside builder
let wrongModel = MonteCarloExpressionModel { builder in
let rate = 0.05
let years = 10.0
let multiplier = (1.0 + rate).power(years) // ERROR! Can’t call .power() on Double
return builder[0] * multiplier
}
// ✓ CORRECT: Pre-compute constants outside
let rate = 0.05
let years = 10.0
let growthFactor = pow(1.0 + rate, years) // Swift’s pow() for constants
let correctModel = MonteCarloExpressionModel { builder in
let principal = builder[0]
return principal * growthFactor // Use pre-computed constant
}
This design enables the GPU to run at maximum speed - constants are embedded in the bytecode, and only the randomized variables are computed per iteration.
─────────────────────────────────────────────────
When Expression Models Aren’t Appropriate:
The revenue growth forecast we showed earlier requires traditional loops:
// This REQUIRES traditional loops (compounding across periods)
for _ in 0…(iterations - 1) {
var currentRevenue = baseRevenue
for period in periods {
let growth = sampleGrowth()
currentRevenue *= (1.0 + growth) // State carries forward!
record(period, currentRevenue)
}
}
Why? Each period’s value depends on the previous period’s outcome. Expression models excel at independent calculations but can’t handle this kind of path-dependent compounding.
The right tool for the job:
- Compounding forecasts → Traditional loops
- Single-period high-throughput → Expression models
- Complex multi-period dependencies → Traditional loops
- Simple formulas with 100K+ iterations → Expression models
Try It Yourself
Click to expand full playground code
import BusinessMath
// MARK: - Single Metric with Growth Uncertainty
// Historical revenue
let baseRevenue = 1_000_000.0 // $1M
// Growth rate uncertainty: mean 10%, std dev 5%
let growthDriver = ProbabilisticDriver
.normal(
name: "Quarterly Growth",
mean: 0.10, // Expected 10% per quarter
stdDev: 0.05 // ±5% uncertainty
)
// Project 4 quarters
let q1 = Period.quarter(year: 2025, quarter: 1)
let quarters = [q1, q1 + 1, q1 + 2, q1 + 3]
// Run Monte Carlo simulation (10,000 paths)
let iterations = 10_000
// Pre-allocate for performance
var allValues: [[Double]] = Array(repeating: [], count: quarters.count)
for i in 0...(quarters.count - 1) {
allValues[i].reserveCapacity(iterations)
}
// Generate revenue paths with compounding
for _ in 0...(iterations - 1) {
var currentRevenue = baseRevenue
for (periodIndex, period) in quarters.enumerated() {
let growth = growthDriver.sample(for: period)
currentRevenue = currentRevenue * (1.0 + growth) // Compound!
allValues[periodIndex].append(currentRevenue)
}
}
// Calculate statistics for each period
var statistics: [Period: SimulationStatistics] = [:]
var percentiles: [Period: Percentiles] = [:]
for (periodIndex, period) in quarters.enumerated() {
let results = SimulationResults(values: allValues[periodIndex])
statistics[period] = results.statistics
percentiles[period] = results.percentiles
}
// Display results
print("Revenue Forecast with Compounding Growth")
print("=========================================")
print("Base Revenue: \(baseRevenue.currency(0))")
print("Quarterly Growth: 10% ± 5% (compounding)")
print()
print("Quarter Mean Median 90% CI Growth")
print("------- ---------- ---------- ------------------------ -------")
for quarter in quarters {
let stats = statistics[quarter]!
let pctiles = percentiles[quarter]!
let growth = (stats.mean - baseRevenue) / baseRevenue
print("\(quarter.label) \(stats.mean.currency(0)) \(pctiles.p50.currency(0)) [\(pctiles.p5.currency(0)), \(pctiles.p95.currency(0))] \(growth.percent(1))")
}
// MARK: - Extract Scenario Time Series
// Build time series at different confidence levels
let expectedValues = quarters.map { statistics[$0]!.mean }
let medianValues = quarters.map { percentiles[$0]!.p50 }
let p5Values = quarters.map { percentiles[$0]!.p5 }
let p95Values = quarters.map { percentiles[$0]!.p95 }
let expectedRevenue = TimeSeries(periods: quarters, values: expectedValues)
let medianRevenue = TimeSeries(periods: quarters, values: medianValues)
let conservativeRevenue = TimeSeries(periods: quarters, values: p5Values)
let optimisticRevenue = TimeSeries(periods: quarters, values: p95Values)
print("\nScenario Projections:")
print("Conservative (P5): \(conservativeRevenue.valuesArray.map { $0.currency(0) })")
print("Median (P50): \(medianRevenue.valuesArray.map { $0.currency(0) })")
print("Expected (mean): \(expectedRevenue.valuesArray.map { $0.currency(0) })")
print("Optimistic (P95): \(optimisticRevenue.valuesArray.map { $0.currency(0) })")
// MARK: - Complete Income Statement Forecast
// Define probabilistic drivers
struct IncomeStatementDrivers {
let unitsSold: ProbabilisticDriver
let averagePrice: ProbabilisticDriver
let cogs: ProbabilisticDriver
// % of revenue
let opex: ProbabilisticDriver
init() {
// Units: Normal distribution (mean 10K, std 1K)
self.unitsSold = .normal(
name: "Units Sold",
mean: 10_000.0,
stdDev: 1_000.0
)
// Price: Triangular distribution (most likely $100, range $95-$110)
self.averagePrice = .triangular(
name: "Average Price",
low: 95.0,
high: 110.0,
base: 100.0
)
// COGS as % of revenue: Normal (mean 60%, std 3%)
self.cogs = .normal(
name: "COGS %",
mean: 0.60,
stdDev: 0.03
)
// OpEx: Normal (mean $200K, std $20K)
self.opex = .normal(
name: "Operating Expenses",
mean: 200_000.0,
stdDev: 20_000.0
)
}
}
let drivers = IncomeStatementDrivers()
let periods = Period.year(2025).quarters()
// Run simulation manually for full control
var revenueValues: [[Double]] = Array(repeating: [], count: periods.count)
var grossProfitValues: [[Double]] = Array(repeating: [], count: periods.count)
var opIncomeValues: [[Double]] = Array(repeating: [], count: periods.count)
for i in 0...(periods.count - 1) {
revenueValues[i].reserveCapacity(iterations)
grossProfitValues[i].reserveCapacity(iterations)
opIncomeValues[i].reserveCapacity(iterations)
}
for _ in 0...(iterations - 1) {
for (periodIndex, period) in periods.enumerated() {
// Sample all drivers
let units = drivers.unitsSold.sample(for: period)
let price = drivers.averagePrice.sample(for: period)
let cogsPercent = drivers.cogs.sample(for: period)
let opexAmount = drivers.opex.sample(for: period)
// Calculate P&L
let revenue = units * price
let grossProfit = revenue * (1.0 - cogsPercent)
let operatingIncome = grossProfit - opexAmount
// Record
revenueValues[periodIndex].append(revenue)
grossProfitValues[periodIndex].append(grossProfit)
opIncomeValues[periodIndex].append(operatingIncome)
}
}
// Calculate statistics
var revenueStats: [Period: SimulationStatistics] = [:]
var revenuePctiles: [Period: Percentiles] = [:]
var gpStats: [Period: SimulationStatistics] = [:]
var gpPctiles: [Period: Percentiles] = [:]
var opStats: [Period: SimulationStatistics] = [:]
var opPctiles: [Period: Percentiles] = [:]
for (periodIndex, period) in periods.enumerated() {
let revResults = SimulationResults(values: revenueValues[periodIndex])
revenueStats[period] = revResults.statistics
revenuePctiles[period] = revResults.percentiles
let gpResults = SimulationResults(values: grossProfitValues[periodIndex])
gpStats[period] = gpResults.statistics
gpPctiles[period] = gpResults.percentiles
let opResults = SimulationResults(values: opIncomeValues[periodIndex])
opStats[period] = opResults.statistics
opPctiles[period] = opResults.percentiles
}
// Display comprehensive forecast
print("\nIncome Statement Forecast - 2025")
print("==================================")
for quarter in periods {
print("\n\(quarter.label)")
print(String(repeating: "-", count: 60))
// Revenue
let revS = revenueStats[quarter]!
let revP = revenuePctiles[quarter]!
print("Revenue")
print(" Expected: \(revS.mean.currency(0))")
print(" Std Dev: \(revS.stdDev.currency(0)) (CoV: \((revS.stdDev / revS.mean).percent(1)))")
print(" 90% CI: [\(revP.p5.currency(0)), \(revP.p95.currency(0))]")
// Gross Profit
let gpS = gpStats[quarter]!
let gpP = gpPctiles[quarter]!
let gpMargin = gpS.mean / revS.mean
print("\nGross Profit")
print(" Expected: \(gpS.mean.currency(0)) (\(gpMargin.percent(1)) margin)")
print(" 90% CI: [\(gpP.p5.currency(0)), \(gpP.p95.currency(0))]")
// Operating Income
let opS = opStats[quarter]!
let opP = opPctiles[quarter]!
let opMargin = opS.mean / revS.mean
print("\nOperating Income")
print(" Expected: \(opS.mean.currency(0)) (\(opMargin.percent(1)) margin)")
print(" 90% CI: [\(opP.p5.currency(0)), \(opP.p95.currency(0))]")
// Risk assessment
let profitProb = opP.p5 > 0 ? 100 : (opP.p25 > 0 ? 75 : (opP.p50 > 0 ? 50 : 25))
print("\nRisk: Probability of profit ~\(profitProb)%")
}
// MARK: - GPU-Accelerated Expression Models
// Pre-compute any constants
let taxRate = 0.21
// Define the P&L model using expression builder
let incomeStatementModel = MonteCarloExpressionModel { builder in
// Inputs: units, price, cogsPercent, opex
let units = builder[0]
let price = builder[1]
let cogsPercent = builder[2]
let opex = builder[3]
// Calculate revenue and costs
let revenue = units * price
let cogs = revenue * cogsPercent
let grossProfit = revenue - cogs
let ebitda = grossProfit - opex
// Conditional tax (only pay tax if profitable)
let isProfitable = ebitda.greaterThan(0.0)
let tax = isProfitable.ifElse(
then: ebitda * taxRate,
else: 0.0
)
let netIncome = ebitda - tax
return netIncome // Return what we're simulating
}
// Set up high-performance simulation
var gpuSimulation = MonteCarloSimulation(
iterations: 100_000, // 10× more iterations than before
enableGPU: true,
expressionModel: incomeStatementModel
)
// Add input distributions (order matches builder[0], builder[1], etc.)
gpuSimulation.addInput(SimulationInput(
name: "Units Sold",
distribution: DistributionNormal(mean: 10_000, stdDev: 1_000)
))
gpuSimulation.addInput(SimulationInput(
name: "Average Price",
distribution: DistributionTriangular(low: 95, high: 110, mode: 100)
))
gpuSimulation.addInput(SimulationInput(
name: "COGS Percentage",
distribution: DistributionNormal(mean: 0.60, stdDev: 0.03)
))
gpuSimulation.addInput(SimulationInput(
name: "Operating Expenses",
distribution: DistributionNormal(mean: 200_000, stdDev: 20_000)
))
// Run simulation
let gpuResults = try gpuSimulation.run()
// Display results
print("\n\nGPU-Accelerated Income Statement Forecast")
print("==========================================")
print("Iterations: \(gpuResults.iterations.formatted())")
print("Compute Time: \(gpuResults.computeTime.formatted(.number.precision(.fractionLength(1)))) ms")
print("GPU Used: \(gpuResults.usedGPU ? "Yes" : "No")")
print()
print("Net Income After Tax:")
print(" Mean: \(gpuResults.statistics.mean.currency(0))")
print(" Median: \(gpuResults.percentiles.p50.currency(0))")
print(" Std Dev: \(gpuResults.statistics.stdDev.currency(0))")
print(" 95% CI: [\(gpuResults.percentiles.p5.currency(0)), \(gpuResults.percentiles.p95.currency(0))]")
print()
// Risk metrics
let profitableCount = gpuResults.valuesArray.filter { $0 > 0 }.count
let profitabilityRate = Double(profitableCount) / Double(gpuResults.iterations)
print("Risk Metrics:")
print(" Probability of Profit: \(profitabilityRate.percent(1))")
print(" Value at Risk (5%): \(gpuResults.percentiles.p5.currency(0))")
Modifications to try:
- Add correlation between drivers (revenue and costs often move together)
- Model mean-reverting growth (growth rate reverts to long-term average)
- Add extreme event scenarios (5% chance of 50% revenue drop)
- Build multi-year forecasts with changing distributions over time
Real-World Application
Every CFO, risk manager, and strategic planner uses Monte Carlo:- Annual budgeting: “What’s the 80% confidence interval for EBITDA?”
- Capital allocation: “How likely is ROI > 15%?”
- Risk management: “What’s the worst-case revenue in the bottom 5% of scenarios?”
- Strategic planning: “If we enter this market, what’s the probability of profitability by year 3?”
BusinessMath makes Monte Carlo forecasting programmatic, reproducible, and fast.
★ Insight ─────────────────────────────────────
Why Monte Carlo Beats Scenario Analysis
Traditional approach: Build 3 scenarios (base, best, worst).
Problems:
- No probabilities: Is “best case” 90th percentile or 99th?
- Arbitrary combinations: Best case has high revenue AND low costs (unlikely!)
- Missed interactions: When revenue is high, costs often are too (correlation ignored)
- Explicit probabilities: P90 means “exceeded 90% of the time”
- Natural combinations: High revenue scenario automatically samples from the high end of the revenue distribution
- Captures correlation: Model correlated drivers with copulas or factor models
─────────────────────────────────────────────────
📝 Development Note
The biggest challenge here balancing ease-of-use with flexibility. We could have provided:Option A: High-level forecastRevenue(baseAmount, growthDist, periods)
- Pro: Very easy to use
- Con: Inflexible (what if growth depends on prior period revenue?)
- Pro: Maximum flexibility
- Con: Users must write boilerplate for every forecast
ProbabilisticDriver,
SimulationResults) that handle the tedious parts (sampling, statistics) while leaving control over the simulation logic. Even though it’s a step away from the expressiveness of pure swift functions, the power boost is massive, and while still retaining the benefits of reusability.
Related Methodology: Test-First Development (Week 1) - We wrote tests comparing Monte Carlo results to analytical solutions (e.g., normal distribution revenue forecast) before implementing.
Next Steps
Coming up Wednesday: Scenario Analysis - Building discrete scenarios, sensitivity analysis, and tornado diagrams.Friday: Case Study #3 - Option Pricing with Monte Carlo.
Series Progress:
- Week: 6/12
- Posts Published: 20/~48
- Topics Covered: Foundation + Analysis + Operational + Financial Statements + Advanced Modeling + Simulation (starting)
- Playgrounds: 19 available
Tagged with: risk-and-simulation