Risk Analytics and Stress Testing

BusinessMath Quarterly Series

13 min read

Part 8 of 12-Week BusinessMath Series


What You’ll Learn


The Problem

Risk management requires quantifying uncertainty. What’s the worst loss we might face? How would a recession affect our portfolio? Are we properly diversified?

Traditional risk analysis involves complex calculations:

Implementing these correctly requires statistical knowledge, careful handling of distributions, and proper correlation modeling. You need production-ready risk analytics without reinventing the math.

The Solution

BusinessMath provides comprehensive risk analytics including stress testing, VaR calculation, and multi-portfolio risk aggregation.

Stress Testing

Evaluate how portfolios perform under adverse scenarios:
import BusinessMath

// Pre-defined stress scenarios
var allScenarios = [
StressScenario .recession, // Moderate economic downturn
StressScenario .crisis, // Severe financial crisis
StressScenario .supplyShock // Supply chain disruption
]

// Examine scenario parameters
for scenario in scenarios {
print(”(scenario.name):”)
print(” Description: (scenario.description)”)
print(” Shocks:”)
for (driver, shock) in scenario.shocks {
let pct = shock * 100
print(” (driver): (pct > 0 ? “+” : “”)(pct)%”)
}
}
Output:
Recession:
Description: Economic recession scenario
Shocks:
Revenue: -15.0%
COGS: +5.0%
InterestRate: +2.0%

Financial Crisis:
Description: Severe financial crisis (2008-style)
Shocks:
Revenue: -30.0%
InterestRate: +5.0%
CustomerChurn: +20.0%
COGS: +10.0%

Supply Chain Shock:
Description: Major supply chain disruption
Shocks:
InventoryLevel: -30.0%
DeliveryTime: +50.0%
COGS: +25.0%

Custom Stress Scenarios

Create scenarios specific to your business:
// Pandemic scenario
let pandemic = StressScenario(
name: “Global Pandemic”,
description: “Extended lockdowns and remote work transition”,
shocks: [
“Revenue”: -0.35, // -35% revenue
“RemoteWorkCosts”: 0.20, // +20% IT/remote costs
“TravelExpenses”: -0.80, // -80% travel
“RealEstateCosts”: -0.15 // -15% office costs
]
)

allScenarios.append(pandemic)

// Regulatory change scenario
let regulation = StressScenario(
name: “New Regulation”,
description: “Stricter compliance requirements”,
shocks: [
“ComplianceCosts”: 0.50, // +50% compliance
“Revenue”: -0.05, // -5% from restrictions
“OperatingMargin”: -0.03 // -3% margin compression
]
)
allScenarios.append(regulation)

Running Stress Tests

Apply scenarios to your financial model:
let stressTest = StressTest(scenarios: allScenarios)

struct FinancialMetrics {
var revenue: Double
var costs: Double
var npv: Double
}

let baseline = FinancialMetrics(
revenue: 10_000_000,
costs: 7_000_000,
npv: 5_000_000
)

for scenario in stressTest.scenarios {
// Apply shocks
var stressed = baseline

if let revenueShock = scenario.shocks[“Revenue”] {
stressed.revenue *= (1 + revenueShock)
}

if let cogsShock = scenario.shocks[“COGS”] {
stressed.costs *= (1 + cogsShock)
}

let stressedNPV = stressed.revenue - stressed.costs
let impact = stressedNPV - baseline.npv
let impactPct = (impact / baseline.npv)

print(”\n(scenario.name):”)
print(” Baseline NPV: (baseline.npv.currency())”)
print(” Stressed NPV: (stressedNPV.currency())”)
print(” Impact: (impact.currency()) ((impactPct.percent()))”)
}

Value at Risk (VaR)

VaR measures the maximum loss expected over a time horizon at a given confidence level.

S&P Returns Data

Calculating VaR from Returns

// Portfolio returns (historical daily returns)
let spReturns: [Double] = [0.0088, 0.0079, -0.0116…] //(See file for data)

let periods = (0…(spReturns.count - 1)).map {
Period.day(Date().addingTimeInterval(Double($0) * 86400))
}
let timeSeries = TimeSeries(periods: periods, values: spReturns)

let riskMetrics = ComprehensiveRiskMetrics(
returns: timeSeries,
riskFreeRate: 0.02 / 250 // 2% annual = 0.008% daily
)

print(“Value at Risk:”)
print(” 95% VaR: (riskMetrics.var95.percent())”)
print(” 99% VaR: (riskMetrics.var99.percent())”)

// Interpret: “95% confidence we won’t lose more than X% in a day”
let portfolioValue = 1_000_000.0
let var95Loss = abs(riskMetrics.var95) * portfolioValue

print(”\nFor (portfolioValue.currency(0)) portfolio:”)
print(” 95% 1-day VaR: (var95Loss.currency())”)
print(” Meaning: 95% confident daily loss won’t exceed (var95Loss.currency())”)

Conditional VaR (CVaR / Expected Shortfall)

CVaR measures the average loss in the worst cases (beyond VaR):
print(”\nConditional VaR (Expected Shortfall):”)
print(” CVaR (95%): (riskMetrics.cvar95.percent())”)
print(” Tail Risk Ratio: (riskMetrics.tailRisk.number())”)

// CVaR is the expected loss if we’re in the worst 5%
let cvarLoss = abs(riskMetrics.cvar95) * portfolioValue
print(” If in worst 5% of days, expect to lose: (cvarLoss.currency())”)
CVaR is better than VaR because it captures tail risk—the average loss when things go really bad, not just the threshold.

Comprehensive Risk Metrics

Get a complete risk profile:
print(”\nComprehensive Risk Profile:”)
print(riskMetrics.description)
Output:
Comprehensive Risk Profile:
Comprehensive Risk Metrics:
VaR (95%): -1.66%
VaR (99%): -4.84%
CVaR (95%): -2.76%
Max Drawdown: 18.91%
Sharpe Ratio: 0.05
Sortino Ratio: 0.05
Tail Risk: 1.66
Skewness: 1.05
Kurtosis: 18.53

Maximum Drawdown

Maximum drawdown measures the largest peak-to-trough decline:
let drawdown = riskMetrics.maxDrawdown

print(”\nDrawdown Analysis:”)
print(” Maximum drawdown: (drawdown.percent())”)

if drawdown < 0.10 {
print(” Risk level: Low”)
} else if drawdown < 0.20 {
print(” Risk level: Moderate”)
} else {
print(” Risk level: High”)
}

Sharpe and Sortino Ratios

Risk-adjusted return measures:
print(”\nRisk-Adjusted Returns:”)
print(” Sharpe Ratio: (riskMetrics.sharpeRatio.number(3))”)
print(” (return per unit of total volatility)”)

print(” Sortino Ratio: (riskMetrics.sortinoRatio.number(3))”)
print(” (return per unit of downside volatility)”)

// Sortino > Sharpe indicates asymmetric returns (positive skew)
if riskMetrics.sortinoRatio > riskMetrics.sharpeRatio {
print(” Portfolio has limited downside with upside potential”)
}
Sharpe Ratio penalizes all volatility (up and down). Sortino Ratio only penalizes downside volatility—better for assessing asymmetric strategies.

Tail Statistics

Skewness and kurtosis describe return distribution shape:
print(”\nTail Statistics:”)
print(” Skewness: (riskMetrics.skewness)”)

if riskMetrics.skewness < -0.5 {
print(” Negative skew: More frequent small gains, rare large losses”)
print(” Risk: Fat left tail”)
} else if riskMetrics.skewness > 0.5 {
print(” Positive skew: More frequent small losses, rare large gains”)
print(” Risk: Fat right tail”)
} else {
print(” Roughly symmetric distribution”)
}

print(” Excess Kurtosis: (riskMetrics.kurtosis)”)

if riskMetrics.kurtosis > 1.0 {
print(” Fat tails: More extreme events than normal distribution”)
print(” Risk: Higher probability of large moves”)
}

Aggregating Risk Across Portfolios

Combine VaR across multiple portfolios accounting for correlations:
// Three portfolios with individual VaRs
let portfolioVaRs = [100_000.0, 150_000.0, 200_000.0]

// Correlation matrix
let correlations = [
[1.0, 0.6, 0.4],
[0.6, 1.0, 0.5],
[0.4, 0.5, 1.0]
]

// Aggregate VaR using variance-covariance method
let aggregatedVaR = RiskAggregator .aggregateVaR(
individualVaRs: portfolioVaRs,
correlations: correlations
)

let simpleSum = portfolioVaRs.reduce(0, +)
let diversificationBenefit = simpleSum - aggregatedVaR

print(“VaR Aggregation:”)
print(” Portfolio A VaR: (portfolioVaRs[0].currency())”)
print(” Portfolio B VaR: (portfolioVaRs[1].currency())”)
print(” Portfolio C VaR: (portfolioVaRs[2].currency())”)
print(” Simple sum: (simpleSum.currency())”)
print(” Aggregated VaR: (aggregatedVaR.currency())”)
print(” Diversification benefit: (diversificationBenefit.currency())”)
Diversification benefit shows how much risk is reduced by not being perfectly correlated.

Marginal VaR

Understand how much each portfolio contributes to total risk:
for i in 0..
            
               let marginal = RiskAggregator
              
                .marginalVaR(
                
entity: i,
individualVaRs: portfolioVaRs,
correlations: correlations
)

print(”\nPortfolio ([“A”, “B”, “C”][i]):”)
print(” Individual VaR: (portfolioVaRs[i].currency())”)
print(” Marginal VaR: (marginal.currency())”)
print(” Risk contribution: ((marginal / aggregatedVaR).percent())”)
}
Marginal VaR tells you: “If I added $1 more to this portfolio, how much would total VaR increase?”

Try It Yourself

S&P Returns Data (add to the /Sources file of your playground)
Click to expand full playground code
import BusinessMath

// Pre-defined stress scenarios
var allScenarios = [
StressScenario .recession, // Moderate economic downturn
StressScenario .crisis, // Severe financial crisis
StressScenario .supplyShock // Supply chain disruption
]

// Examine scenario parameters
for scenario in allScenarios {
print("\(scenario.name):")
print(" Description: \(scenario.description)")
print(" Shocks:")
for (driver, shock) in scenario.shocks {
let pct = shock * 100
print(" \(driver): \(pct > 0 ? "+" : "")\(pct)%")
}
}

// Pandemic scenario
let pandemic = StressScenario(
name: "Global Pandemic",
description: "Extended lockdowns and remote work transition",
shocks: [
"Revenue": -0.35, // -35% revenue
"RemoteWorkCosts": 0.20, // +20% IT/remote costs
"TravelExpenses": -0.80, // -80% travel
"RealEstateCosts": -0.15 // -15% office costs
]
)
allScenarios.append(pandemic)

// Regulatory change scenario
let regulation = StressScenario(
name: "New Regulation",
description: "Stricter compliance requirements",
shocks: [
"ComplianceCosts": 0.50, // +50% compliance
"Revenue": -0.05, // -5% from restrictions
"OperatingMargin": -0.03 // -3% margin compression
]
)
allScenarios.append(regulation)

let stressTest = StressTest(scenarios: allScenarios)

struct FinancialMetrics {
var revenue: Double
var costs: Double
var npv: Double
}

let baseline = FinancialMetrics(
revenue: 10_000_000,
costs: 7_000_000,
npv: 5_000_000
)

for scenario in stressTest.scenarios {
// Apply shocks
var stressed = baseline

if let revenueShock = scenario.shocks["Revenue"] {
stressed.revenue *= (1 + revenueShock)
}

if let cogsShock = scenario.shocks["COGS"] {
stressed.costs *= (1 + cogsShock)
}

let stressedNPV = stressed.revenue - stressed.costs
let impact = stressedNPV - baseline.npv
let impactPct = (impact / baseline.npv)

print("\n\(scenario.name):")
print(" Baseline NPV: \(baseline.npv.currency())")
print(" Stressed NPV: \(stressedNPV.currency())")
print(" Impact: \(impact.currency()) (\(impactPct.percent()))")
}

// Portfolio returns (historical daily returns) come from Sources: spReturns: [Double]
let periods: [Period] = (0.. Period.day(Date().addingTimeInterval(Double(idx) * 86_400))
}
let timeSeries: TimeSeries = TimeSeries(periods: periods, values: spReturns)

let riskMetrics = ComprehensiveRiskMetrics(
returns: timeSeries,
riskFreeRate: 0.02 / 250 // 2% annual = 0.008% daily
)
print("Value at Risk:")
print(" 95% VaR: \(riskMetrics.var95.percent())")
print(" 99% VaR: \(riskMetrics.var99.percent())")

// Interpret: "95% confidence we won't lose more than X% in a day"
let portfolioValue = 1_000_000.0
let var95Loss = abs(riskMetrics.var95) * portfolioValue

print("\nFor \(portfolioValue.currency(0)) portfolio:")
print(" 95% 1-day VaR: \(var95Loss.currency())")
print(" Meaning: 95% confident daily loss won't exceed \(var95Loss.currency())")

print("\nConditional VaR (Expected Shortfall):")
print(" CVaR (95%): \(riskMetrics.cvar95.percent())")
print(" Tail Risk Ratio: \(riskMetrics.tailRisk.number())")

// CVaR is the expected loss if we're in the worst 5%
let cvarLoss = abs(riskMetrics.cvar95) * portfolioValue
print(" If in worst 5% of days, expect to lose: \(cvarLoss.currency())")


print("\nComprehensive Risk Profile:")
print(riskMetrics.description)

let drawdown = riskMetrics.maxDrawdown

print("\nDrawdown Analysis:")
print(" Maximum drawdown: \(drawdown.percent())")

if drawdown < 0.10 {
print(" Risk level: Low")
} else if drawdown < 0.20 {
print(" Risk level: Moderate")
} else {
print(" Risk level: High")
}

print("\nRisk-Adjusted Returns:")
print(" Sharpe Ratio: \(riskMetrics.sharpeRatio.number(3))")
print(" (return per unit of total volatility)")

print(" Sortino Ratio: \(riskMetrics.sortinoRatio.number(3))")
print(" (return per unit of downside volatility)")

// Sortino > Sharpe indicates asymmetric returns (positive skew)
if riskMetrics.sortinoRatio > riskMetrics.sharpeRatio {
print(" Portfolio has limited downside with upside potential")
}

print("\nTail Statistics:")
print(" Skewness: \(riskMetrics.skewness.number(2))")

if riskMetrics.skewness < -0.5 {
print(" Negative skew: More frequent small gains, rare large losses")
print(" Risk: Fat left tail")
} else if riskMetrics.skewness > 0.5 {
print(" Positive skew: More frequent small losses, rare large gains")
print(" Risk: Fat right tail")
} else {
print(" Roughly symmetric distribution")
}

print(" Excess Kurtosis: \(riskMetrics.kurtosis.number(2))")

if riskMetrics.kurtosis > 1.0 {
print(" Fat tails: More extreme events than normal distribution")
print(" Risk: Higher probability of large moves")
}

// Three portfolios with individual VaRs
let portfolioVaRs = [100_000.0, 150_000.0, 200_000.0]

// Correlation matrix
let correlations = [
[1.0, 0.6, 0.4],
[0.6, 1.0, 0.5],
[0.4, 0.5, 1.0]
]

// Aggregate VaR using variance-covariance method
let aggregatedVaR = RiskAggregator .aggregateVaR(
individualVaRs: portfolioVaRs,
correlations: correlations
)

let simpleSum = portfolioVaRs.reduce(0, +)
let diversificationBenefit = simpleSum - aggregatedVaR

print("VaR Aggregation:")
print(" Portfolio A VaR: \(portfolioVaRs[0].currency())")
print(" Portfolio B VaR: \(portfolioVaRs[1].currency())")
print(" Portfolio C VaR: \(portfolioVaRs[2].currency())")
print(" Simple sum: \(simpleSum.currency())")
print(" Aggregated VaR: \(aggregatedVaR.currency())")
print(" Diversification benefit: \(diversificationBenefit.currency())")

for i in 0.. let marginal = RiskAggregator .marginalVaR(
entity: i,
individualVaRs: portfolioVaRs,
correlations: correlations
)

print("\nPortfolio \(["A", "B", "C"][i]):")
print(" Individual VaR: \(portfolioVaRs[i].currency())")
print(" Marginal VaR: \(marginal.currency())")
print(" Risk contribution: \((marginal / aggregatedVaR).percent())")
}

→ Full API Reference: BusinessMath Docs – 2.3 Risk Analytics

Modifications to try:

  1. Create custom stress scenarios for your industry
  2. Calculate VaR and CVaR for different confidence levels
  3. Compare Sharpe vs Sortino ratios for asymmetric strategies

Real-World Application

Risk managers use these tools daily: BusinessMath makes these institutional-grade analytics accessible in 10-20 lines of Swift code.
★ Insight ─────────────────────────────────────

Why Both VaR and CVaR?

VaR answers: “What’s the threshold of the worst 5% of outcomes?” CVaR answers: “When you’re in that worst 5%, how bad does it actually get?”

Example: Portfolio with VaR₉₅ = -$100k, CVaR₉₅ = -$500k

CVaR captures tail risk—the thing that kills portfolios. VaR alone can be misleading for fat-tailed distributions.

This distinction matters for crypto, options, and leveraged strategies where tails are fat.

─────────────────────────────────────────────────


📝 Development Note

The hardest part of implementing VaR wasn’t the math—it was choosing which variant to implement. There are three common methods:
  1. Historical VaR: Use actual historical percentile
  2. Parametric VaR: Assume normal distribution
  3. Monte Carlo VaR: Simulate future scenarios
We chose Historical VaR as the default because: But we documented this choice explicitly in both code and DocC, so users know what they’re getting.

The lesson: When multiple valid implementations exist, pick one, document it clearly, and make the choice transparent.

Related Methodology: When Tests Pass But Code Is Wrong (Week 9) - Validating statistical implementations


Next Steps

Coming up this week: Week 3 explores operational models—growth, depreciation, and revenue modeling.

Case Study: Week 3 Friday combines depreciation + TVM for capital equipment purchase decisions.


Series Progress:


Tagged with: risk-and-simulation, portfolio