BusinessMath Quarterly Series
10 min read
Part 22 of 12-Week BusinessMath Series
Business optimization is everywhere:
Manual optimization (trial-and-error in Excel) doesn’t scale and misses optimal solutions.
BusinessMath provides a 5-phase optimization framework:
Find where a function equals a target value:
import BusinessMath
// Profit function with price elasticity
func profit(price: Double) -> Double {
let quantity = 10_000 - 1_000 * price // Demand curve
let revenue = price * quantity
let fixedCosts = 2_000.0
let variableCost = 5.0
let totalCosts = fixedCosts + variableCost * quantity
return revenue - totalCosts
}
// Find breakeven price (profit = 0)
let breakevenPrice = try goalSeek(
function: profit,
target: 0.0,
guess: 10.0,
tolerance: 0.01
)
print("Breakeven price: \(breakevenPrice.currency(2))")
Output:
Breakeven price: $9.56
The method: Uses bisection + Newton-Raphson hybrid for robust convergence.
Internal Rate of Return is a goal-seek problem (find rate where NPV = 0):
let cashFlows = [-1_000.0, 200.0, 300.0, 400.0, 500.0]
func npv(rate: Double) -> Double {
var npv = 0.0
for (t, cf) in cashFlows.enumerated() {
npv += cf / pow(1 + rate, Double(t))
}
return npv
}
// Find rate where NPV = 0
let irr = try goalSeek(
function: npv,
target: 0.0,
guess: 0.10
)
print("IRR: \(irr.percent(2))")
Output:
IRR: 12.83%
Multivariate optimization requires vector operations:
// Create vectors
let v = VectorN([3.0, 4.0])
let w = VectorN([1.0, 2.0])
// Basic operations
let sum = v + w // [4, 6]
let scaled = 2.0 * v // [6, 8]
// Norms and distances
print("Norm: \(v.norm)") // 5.0
print("Distance: \(v.distance(to: w))") // 2.828
print("Dot product: \(v.dot(w))") // 11.0
Application - Portfolio weights:
let weights = VectorN([0.25, 0.30, 0.25, 0.20])
let returns = VectorN([0.12, 0.15, 0.10, 0.18])
// Portfolio return (weighted average)
let portfolioReturn = weights.dot(returns)
print("Portfolio return: \(portfolioReturn.percent(1))") // 13.6%
Optimize functions of multiple variables:
// Minimize Rosenbrock function (classic test problem)
let rosenbrock: (VectorN) -> Double = { v in
let x = v[0], y = v[1]
let a = 1 - x
let b = y - x*x
return a*a + 100*b*b // Minimum at (1, 1)
}
// Adam optimizer (adaptive learning rate)
let optimizer = MultivariateGradientDescent>(
learningRate: 0.01,
maxIterations: 10_000
)
let result = try optimizer.minimizeAdam(
function: rosenbrock,
initialGuess: VectorN([0.0, 0.0])
)
print("Solution: \(result.solution.toArray())") // ~[1, 1]
print("Iterations: \(result.iterations)")
print("Final value: \(result.value)")
Output:
Solution: [0.9999990406781208, 0.9999980785494371]
Iterations: 704
Final value: 9.210867997017215e-13```
**The power**: Adam finds the minimum automatically with no manual tuning.
---
### BFGS for Smooth Functions
For smooth, well-behaved functions, BFGS converges faster:
```swift
// Quadratic function: f(x) = x^T A x
let A = [[2.0, 0.0, 0.0],
[0.0, 3.0, 0.0],
[0.0, 0.0, 4.0]]
let quadratic: (VectorN) -> Double = { v in
var result = 0.0
for i in 0..<3 {
for j in 0..<3 {
result += v[i] * A[i][j] * v[j]
}
}
return result
}
let bfgs = MultivariateNewtonRaphson>(
maxIterations: 50
)
let resultBFGS = try bfgs.minimize(
quadratic,
from: VectorN([5.0, 5.0, 5.0])
)
print("Converged in \(result.iterations) iterations")
print("Solution: \(result.solution.toArray())") // ~[0, 0, 0]
Output:
Converged in 12 iterations
Solution: [0.000, 0.000, 0.000]
The comparison: BFGS took 12 iterations vs. Adam’s 4,782. For smooth functions, second-order methods dominate.
Optimize with equality and inequality constraints:
// Minimize x² + y² subject to x + y = 1
let objective: (VectorN) -> Double = { v in
v[0]*v[0] + v[1]*v[1]
}
let optimizerConstrained = ConstrainedOptimizer>()
let resultConstrained = try optimizerConstrained.minimize(
objective,
from: VectorN([0.0, 1.0]),
subjectTo: [
.equality { v in v[0] + v[1] - 1.0 }
]
)
print("Solution: \(resultConstrained.solution.toArray())") // [0.5, 0.5]
// Shadow price (Lagrange multiplier)
if let lambda = resultConstrained.lagrangeMultipliers.first {
print("Shadow price: \(lambda.number(3))") // How much objective improves if constraint relaxed
}
Output:
Solution: [0.5, 0.5]
Shadow price: 0.500
The interpretation: If we relax the constraint from “x + y = 1” to “x + y = 1.01”, the objective improves by ~0.005 (shadow price × change).
Minimize portfolio risk subject to target return:
let expectedReturns = VectorN([0.08, 0.12, 0.15])
let covarianceMatrix = [
[0.0400, 0.0100, 0.0080],
[0.0100, 0.0900, 0.0200],
[0.0080, 0.0200, 0.1600]
]
// Portfolio variance function
let portfolioVariance: (VectorN) -> Double = { weights in
var variance = 0.0
for i in 0..<3 {
for j in 0..<3 {
variance += weights[i] * weights[j] * covarianceMatrix[i][j]
}
}
return variance
}
let portfolioOptimizer = InequalityOptimizer>()
let result = try portfolioOptimizer.minimize(
portfolioVariance,
from: VectorN([0.4, 0.4, 0.2]),
subjectTo: [
// Target return ≥ 10%
.inequality { w in
let ret = w.dot(expectedReturns)
return 0.10 - ret // ≤ 0 means ret ≥ 10%
},
// Fully invested
.equality { w in w.reduce(0, +) - 1.0 },
// Long-only
.inequality { w in -w[0] },
.inequality { w in -w[1] },
.inequality { w in -w[2] }
]
)
print("Optimal weights: \(result.solution.toArray())")
print("Portfolio variance: \(portfolioVariance(result.solution).number(4))")
print("Portfolio volatility: \((sqrt(portfolioVariance(result.solution))).percent(1))")
Output:
Optimal weights: [0.6099086625245681, 0.2435453283923856, 0.1465460569466559]
Portfolio variance: 0.0295
Portfolio volatility: 17.2%
The solution: 45% in asset 1 (low risk), 35% in asset 2 (medium), 20% in asset 3 (high return). Achieves 10% target return with minimum possible risk.
import BusinessMath
import Foundation
// Profit function with price elasticity
func profit(price: Double) -> Double {
let quantity = 10_000 - 1_000 * price // Demand curve
let revenue = price * quantity
let fixedCosts = 2_000.0
let variableCost = 5.0
let totalCosts = fixedCosts + variableCost * quantity
return revenue - totalCosts
}
// Find breakeven price (profit = 0)
let breakevenPrice = try goalSeek(
function: profit,
target: 0.0,
guess: 10.0,
tolerance: 0.01
)
print("Breakeven price: \(breakevenPrice.currency(2))")
// MARK: - Goal Seeking for IRR
let cashFlows = [-1_000.0, 200.0, 300.0, 400.0, 500.0]
func npv(rate: Double) -> Double {
var npv = 0.0
for (t, cf) in cashFlows.enumerated() {
npv += cf / pow(1 + rate, Double(t))
}
return npv
}
// Find rate where NPV = 0
let irr = try goalSeek(
function: npv,
target: 0.0,
guess: 0.10
)
print("IRR: \(irr.percent(2))")
// MARK: - Vector Operations
// Create vectors
let v = VectorN([3.0, 4.0])
let w = VectorN([1.0, 2.0])
// Basic operations
let sum = v + w // [4, 6]
let scaled = 2.0 * v // [6, 8]
// Norms and distances
print("Norm: \(v.norm)") // 5.0
print("Distance: \(v.distance(to: w))") // 2.828
print("Dot product: \(v.dot(w))") // 11.0
// MARK: - Portfolio Weights
let weights = VectorN([0.25, 0.30, 0.25, 0.20])
let returns = VectorN([0.12, 0.15, 0.10, 0.18])
// Portfolio return (weighted average)
let portfolioReturn = weights.dot(returns)
print("Portfolio return: \(portfolioReturn.percent(1))") // 13.6%
// MARK: - Multivariate Operations
// Minimize Rosenbrock function (classic test problem)
let rosenbrock: (VectorN) -> Double = { v in
let x = v[0], y = v[1]
let a = 1 - x
let b = y - x*x
return a*a + 100*b*b // Minimum at (1, 1)
}
// Adam optimizer (adaptive learning rate)
let optimizer = MultivariateGradientDescent>(
learningRate: 0.01,
maxIterations: 10_000
)
let result = try optimizer.minimizeAdam(
function: rosenbrock,
initialGuess: VectorN([0.0, 0.0])
)
print("Solution: \(result.solution.toArray())") // ~[1, 1]
print("Iterations: \(result.iterations)")
print("Final value: \(result.value)")
// MARK: - BFGS
// Quadratic function: f(x) = x^T A x
let A = [[2.0, 0.0, 0.0],
[0.0, 3.0, 0.0],
[0.0, 0.0, 4.0]]
let quadratic: (VectorN) -> Double = { v in
var result = 0.0
for i in 0..<3 {
for j in 0..<3 {
result += v[i] * A[i][j] * v[j]
}
}
return result
}
let bfgs = MultivariateNewtonRaphson>(
maxIterations: 50
)
let resultBFGS = try bfgs.minimize(
quadratic,
from: VectorN([5.0, 5.0, 5.0])
)
print("Converged in \(resultBFGS.iterations) iterations")
print("Solution: \(resultBFGS.solution.toArray())") // ~[0, 0, 0]
// MARK: - Constrained Optimization
// Minimize x² + y² subject to x + y = 1
let objective: (VectorN) -> Double = { v in
v[0]*v[0] + v[1]*v[1]
}
let optimizerConstrained = ConstrainedOptimizer>()
let resultConstrained = try optimizerConstrained.minimize(
objective,
from: VectorN([0.0, 1.0]),
subjectTo: [
.equality { v in v[0] + v[1] - 1.0 }
]
)
print("Solution: \(resultConstrained.solution.toArray())") // [0.5, 0.5]
// Shadow price (Lagrange multiplier)
if let lambda = resultConstrained.lagrangeMultipliers.first {
print("Shadow price: \(lambda.number(3))") // How much objective improves if constraint relaxed
}
// MARK: - Portfolio with Constraints
let expectedReturns = VectorN([0.08, 0.12, 0.15])
let covarianceMatrix = [
[0.0400, 0.0100, 0.0080],
[0.0100, 0.0900, 0.0200],
[0.0080, 0.0200, 0.1600]
]
// Portfolio variance function
let portfolioVariance: (VectorN) -> Double = { weights in
var variance = 0.0
for i in 0..<3 {
for j in 0..<3 {
variance += weights[i] * weights[j] * covarianceMatrix[i][j]
}
}
return variance
}
let portfolioOptimizer = InequalityOptimizer>()
let resultPortfolio = try portfolioOptimizer.minimize(
portfolioVariance,
from: VectorN([0.4, 0.4, 0.2]),
subjectTo: [
// Target return ≥ 10%
.inequality { w in
let ret = w.dot(expectedReturns)
return 0.10 - ret // ≤ 0 means ret ≥ 10%
},
// Fully invested
.equality { w in w.reduce(0, +) - 1.0 },
// Long-only
.inequality { w in -w[0] },
.inequality { w in -w[1] },
.inequality { w in -w[2] }
]
)
print("Optimal weights: \(resultPortfolio.solution.toArray())")
print("Portfolio variance: \(portfolioVariance(resultPortfolio.solution).number(4))")
print("Portfolio volatility: \((sqrt(portfolioVariance(resultPortfolio.solution))).percent(1))")
→ Full API Reference: BusinessMath Docs – 5.1 Optimization Guide
Modifications to try:
CFO use case: “We manufacture 3 products in 2 factories. Each product has different margins, each factory has capacity constraints. Find the production mix that maximizes EBITDA.”
BusinessMath makes this programmatic, not a manual Excel Solver exercise.
★ Insight ─────────────────────────────────────
Why Second-Order Methods (BFGS) Beat First-Order (Gradient Descent)
Gradient descent uses only the slope (first derivative). BFGS uses the curvature (second derivative via Hessian approximation).
Analogy: Finding the bottom of a valley.
Trade-off: BFGS is faster (fewer iterations) but more complex (memory for Hessian approximation).
Rule of thumb: Use Adam for non-smooth, noisy functions. Use BFGS for smooth, well-behaved functions.
─────────────────────────────────────────────────
The hardest part was choosing default optimization algorithms. We provide multiple (Adam, BFGS, Nelder-Mead, simulated annealing) because no single algorithm dominates:
Rather than pick one “default,” we expose all and provide guidance on when to use each.
Related Methodology: Test-First Development (Week 1) - We tested each optimizer on standard test functions (Rosenbrock, Rastrigin, etc.) with known solutions.
Coming up tomorrow: Portfolio Optimization - Deep dive into Modern Portfolio Theory, efficient frontiers, and risk parity.
Series Progress:
Tagged with: businessmath, swift, optimization, goal-seek, gradient-descent, bfgs, newton-raphson